diff --git a/src/content/mytinytodo.js b/src/content/mytinytodo.js
index bc1b958..50c7b96 100644
--- a/src/content/mytinytodo.js
+++ b/src/content/mytinytodo.js
@@ -312,7 +312,8 @@ var mytinytodo = window.mytinytodo = _mtt = {
$('#tagcloudcontent').on('click', '.tag', function(event){
//tag is not escaped
addFilterTag( this.dataset.tag, this.dataset.tagId, (event.metaKey || event.ctrlKey ? true : false) );
- if(_mtt.menus.tagcloud) _mtt.menus.tagcloud.close();
+ if (_mtt.menus.tagcloud)
+ _mtt.menus.tagcloud.close();
return false;
});
@@ -948,25 +949,32 @@ var mytinytodo = window.mytinytodo = _mtt = {
filter: {
_filters: [],
- clear: function() {
+
+ clear() {
this._filters = [];
$('#mtt-tag-toolbar').hide();
$('#mtt-tag-filters').html('');
},
- addTag: function(tagId, tag, exclude)
+
+ addTag(tagId, tag, exclude)
{
- tagId += 0;
- for (let i in this._filters) {
- if (this._filters[i].tagId && this._filters[i].tagId == tagId)
+ for (const filter of this._filters) {
+ if (filter.tagId && filter.tagId == tagId)
return false;
}
this._filters.push({tagId:tagId, tag:tag, exclude:exclude});
+ if (tagId == -1) {
+ // for display purposes only
+ tag = exclude ? _mtt.lang.get('withAnyTag') : _mtt.lang.get('withoutTags');
+ exclude = false;
+ }
const tagHtml = this.prepareTagHtml(tagId, tag, ['tag-filter', 'tag-id-'+tagId, exclude ? 'tag-filter-exclude' : '']) ;
$('#mtt-tag-filters').append(tagHtml);
$('#mtt-tag-toolbar').show();
return true;
},
- cancelTag: function(tagId)
+
+ cancelTag(tagId)
{
for (let i in this._filters) {
if (this._filters[i].tagId && this._filters[i].tagId == tagId) {
@@ -980,23 +988,25 @@ var mytinytodo = window.mytinytodo = _mtt = {
}
return false;
},
- getTags: function(withExcluded)
+
+ getTags(withExcluded)
{
let a = [];
- for (let i in this._filters) {
- if (this._filters[i].tagId) {
- if (this._filters[i].exclude && withExcluded)
- a.push('^'+ this._filters[i].tag);
- else if (!this._filters[i].exclude)
- a.push(this._filters[i].tag)
+ for (const filter of this._filters) {
+ if (filter.tagId) {
+ if (filter.exclude && withExcluded)
+ a.push('^'+ filter.tag);
+ else if (!filter.exclude)
+ a.push(filter.tag)
}
}
return a.join(', ');
},
- prepareTagHtml: function(tagId, tag, classes)
+
+ prepareTagHtml(tagId, tag, classes)
{
// tag is not escaped
- return '' + escapeHtml(tag) + '';
+ return `${escapeHtml(tag)}`;
}
},
@@ -1988,11 +1998,14 @@ function loadTags(listId, callback)
let cloud = '';
tagsList.forEach( item => {
// item.tag is escaped with htmlspecialchars()
- cloud += ' ' + item.tag + '';
+ cloud += ` ${item.tag}`;
});
if (cloud == '') {
cloud = _mtt.lang.get('noTags');
}
+ else {
+ cloud = `${_mtt.lang.get('withoutTags')}` + cloud;
+ }
$('#tagcloudcontent').html(cloud)
flag.tagsChanged = false;
callback();
@@ -2008,7 +2021,8 @@ function cancelTagFilter(tagId, dontLoadTasks)
function addFilterTag(tag, tagId, exclude)
{
- if(!_mtt.filter.addTag(tagId, tag, exclude)) return false;
+ if (!_mtt.filter.addTag(tagId, tag, exclude))
+ return false;
loadTasks();
};
@@ -2621,7 +2635,7 @@ function slmenuOnListsLoaded()
let s = '';
tabLists.getAll().forEach( (list) => {
const classChecked = (list.id == curList.id) ? 'mtt-item-checked' : '';
- const classHidden = list.hidden ? 'mtt-list-hidden' : '';
+ const classHidden = list.hidden ? 'mtt-list-hidden' : '';
s += `
`;
})
diff --git a/src/includes/api/TasksController.php b/src/includes/api/TasksController.php
index 0a5bc14..1a1a5f1 100644
--- a/src/includes/api/TasksController.php
+++ b/src/includes/api/TasksController.php
@@ -24,7 +24,7 @@ class TasksController extends ApiController {
$db = DBConnection::instance();
$dbcore = DBCore::default();
- $sqlWhere = $sqlWhereListId = '';
+ $sqlWhere = $sqlWhereListId = $sqlHaving = '';
$userLists = [];
if ($listId == -1) {
$userLists = $this->getUserListsSimple();
@@ -46,8 +46,26 @@ class TasksController extends ApiController {
$tagExIds = array();
foreach ($at as $atv) {
$atv = trim($atv);
- if ($atv == '' || $atv == '^') continue;
- if (substr($atv,0,1) == '^') {
+ if ($atv == '')
+ continue;
+ // tasks without tags (ignore other tags included or excluded)
+ if ($atv == '^') {
+ $tagIds = [];
+ $tagExIds = [];
+ if ($db::DBTYPE == DBConnection::DBTYPE_MYSQL)
+ $sqlHaving = "tags_ids = ''";
+ else
+ $sqlHaving = "string_agg(tags.name, ',') IS NULL"; // catches if tag name is ''
+ break;
+ }
+ // tasks with any tag
+ else if ($atv == '^^') {
+ if ($db::DBTYPE == DBConnection::DBTYPE_MYSQL)
+ $sqlHaving = "tags_ids != ''";
+ else
+ $sqlHaving = "string_agg(tags.name, ',') != ''";
+ }
+ else if (substr($atv,0,1) == '^') {
array_push($tagExIds, ...$dbcore->getTagIdsByName(substr($atv,1)));
} else {
$tagIds[] = $dbcore->getTagIdsByName($atv);
@@ -110,6 +128,8 @@ class TasksController extends ApiController {
else {
$groupConcat = "GROUP_CONCAT(tags.id) AS tags_ids, GROUP_CONCAT(tags.name) AS tags";
}
+ if ($sqlHaving != '')
+ $sqlHaving = "HAVING $sqlHaving";
$q = $db->dq("
SELECT todo.*, todo.duedate IS NULL AS ddn, $groupConcat
@@ -117,7 +137,7 @@ class TasksController extends ApiController {
LEFT JOIN {$db->prefix}tag2task AS t2t ON todo.id = t2t.task_id
LEFT JOIN {$db->prefix}tags AS tags ON t2t.tag_id = tags.id
WHERE $sqlWhereListId $sqlWhere
- GROUP BY todo.id
+ GROUP BY todo.id $sqlHaving
$sqlSort
");
diff --git a/src/includes/class.lang.php b/src/includes/class.lang.php
index f43fc2e..d86524b 100644
--- a/src/includes/class.lang.php
+++ b/src/includes/class.lang.php
@@ -127,6 +127,8 @@ class Lang
'listNotFound',
'noPublicLists',
'noTags',
+ 'withoutTags',
+ 'withAnyTag',
'invalidpass',
'addList',
'addListDefault',
diff --git a/src/includes/lang/en.json b/src/includes/lang/en.json
index 9f84948..57c1435 100644
--- a/src/includes/lang/en.json
+++ b/src/includes/lang/en.json
@@ -1,7 +1,7 @@
{
"_header": {
"ver": "v1.8.2",
- "date": "2025-02-15",
+ "date": "2025-02-16",
"language": "English",
"original_name": "English",
"authors": [
@@ -205,6 +205,8 @@
"listNotFound": "List not found",
"noPublicLists": "No public tasks",
"noTags": "No tags",
+ "withoutTags": "No tags",
+ "withAnyTag": "Any tag",
"invalidpass": "Wrong password",
"addList": "Create new list",
"addListDefault": "Todo",
diff --git a/src/includes/lang/ru.json b/src/includes/lang/ru.json
index 11bd3cd..17eaa28 100644
--- a/src/includes/lang/ru.json
+++ b/src/includes/lang/ru.json
@@ -1,7 +1,7 @@
{
"_header": {
"ver": "v1.8.2",
- "date": "2025-02-15",
+ "date": "2025-02-16",
"language": "Russian",
"original_name": "Русский",
"authors": [
@@ -205,6 +205,8 @@
"listNotFound": "Список не найден",
"noPublicLists": "Нет опубликованных списков",
"noTags": "Нет тегов",
+ "withoutTags": "Без тегов",
+ "withAnyTag": "Любой тег",
"invalidpass": "Неверный пароль",
"addList": "Новый список",
"addListDefault": "Todo",