Adds a taglist view with edit/delete buttons

* The tag list can be sort alphabetically or by most used tag
  * Edit/Delete are perform using AJAX, or fallback to 'do=changetag' page
  * New features aren't backported to vintage theme
This commit is contained in:
ArthurHoaro 2017-03-25 15:59:01 +01:00
parent bc988eb042
commit aa4797ba36
9 changed files with 453 additions and 16 deletions

View file

@ -11,7 +11,7 @@
<h2 class="window-title">{"Manage tags"|t}</h2>
<form method="POST" action="#" name="changetag" id="changetag">
<div>
<input type="text" name="fromtag" placeholder="{'Tag'|t}"
<input type="text" name="fromtag" placeholder="{'Tag'|t}" value="{$fromtag}"
list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1">
<datalist id="tagsList">
{loop="$tags"}<option>{$key}</option>{/loop}
@ -31,6 +31,8 @@
<input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete">
</div>
</form>
<p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p>
</div>
</div>
{include="page.footer"}

View file

@ -751,10 +751,11 @@ body, .pure-g [class*="pure-u"] {
.page-form a {
color: #1b926c;
font-weight: bold;
text-decoration: none;
}
.page-form p {
padding: 0 10px;
padding: 5px 10px;
margin: 0;
}
@ -1070,7 +1071,7 @@ form[name="linkform"].page-form {
}
#cloudtag, #cloudtag a {
color: #000;
color: #252525;
text-decoration: none;
}
@ -1078,6 +1079,38 @@ form[name="linkform"].page-form {
color: #7f7f7f;
}
/**
* TAG LIST
*/
#taglist {
padding: 0 10px;
}
#taglist a {
color: #252525;
text-decoration: none;
}
#taglist .count {
display: inline-block;
width: 35px;
text-align: right;
color: #7f7f7f;
}
#taglist .delete-tag {
color: #ac2925;
display: none;
}
#taglist .rename-tag {
color: #0b5ea6;
}
#taglist .validate-rename-tag {
color: #1b926c;
}
/**
* Picture wall CSS
*/
@ -1227,3 +1260,16 @@ form[name="linkform"].page-form {
.pure-button {
-moz-user-select: auto;
}
.tag-sort {
margin-top: 30px;
text-align: center;
}
.tag-sort a {
display: inline-block;
margin: 0 15px;
color: white;
text-decoration: none;
font-weight: bold;
}

View file

@ -412,8 +412,139 @@ window.onload = function () {
}
});
}
/**
* Tag list operations
*
* TODO: support error code in the backend for AJAX requests
*/
// Display/Hide rename form
var renameTagButtons = document.querySelectorAll('.rename-tag');
[].forEach.call(renameTagButtons, function(rename) {
rename.addEventListener('click', function(event) {
event.preventDefault();
var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
var form = block.querySelector('.rename-tag-form');
form.style.display = form.style.display == 'none' ? 'block' : 'none';
});
});
// Rename a tag with an AJAX request
var renameTagSubmits = document.querySelectorAll('.validate-rename-tag');
[].forEach.call(renameTagSubmits, function(rename) {
rename.addEventListener('click', function(event) {
event.preventDefault();
var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
var input = block.querySelector('.rename-tag-input');
var totag = input.value.replace('/"/g', '\\"');
if (totag.trim() == '') {
return;
}
var fromtag = block.getAttribute('data-tag');
var token = document.getElementById('token').value;
xhr = new XMLHttpRequest();
xhr.open('POST', '?do=changetag');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
if (xhr.status !== 200) {
alert('An error occurred. Return code: '+ xhr.status);
location.reload();
} else {
block.setAttribute('data-tag', totag);
input.setAttribute('name', totag);
input.setAttribute('value', totag);
input.parentNode.style.display = 'none';
block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
block.querySelector('a.tag-link').setAttribute('href', '?searchtags='+ encodeURIComponent(totag));
block.querySelector('a.rename-tag').setAttribute('href', '?do=changetag&fromtag='+ encodeURIComponent(totag));
}
};
xhr.send('renametag=1&fromtag='+ encodeURIComponent(fromtag) +'&totag='+ encodeURIComponent(totag) +'&token='+ token);
refreshToken();
});
});
// Validate input with enter key
var renameTagInputs = document.querySelectorAll('.rename-tag-input');
[].forEach.call(renameTagInputs, function(rename) {
rename.addEventListener('keypress', function(event) {
if (event.keyCode === 13) { // enter
findParent(event.target, 'div', {'class': 'tag-list-item'}).querySelector('.validate-rename-tag').click();
}
});
});
// Delete a tag with an AJAX query (alert popup confirmation)
var deleteTagButtons = document.querySelectorAll('.delete-tag');
[].forEach.call(deleteTagButtons, function(rename) {
rename.style.display = 'inline';
rename.addEventListener('click', function(event) {
event.preventDefault();
var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
var tag = block.getAttribute('data-tag');
var token = document.getElementById('token').value;
if (confirm('Are you sure you want to delete the tag "'+ tag +'"?')) {
xhr = new XMLHttpRequest();
xhr.open('POST', '?do=changetag');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
block.remove();
};
xhr.send(encodeURI('deletetag=1&fromtag='+ tag +'&token='+ token));
refreshToken();
}
});
});
};
function findParent(element, tagName, attributes)
{
while (element) {
if (element.tagName.toLowerCase() == tagName) {
var match = true;
for (var key in attributes) {
if (! element.hasAttribute(key)
|| (attributes[key] != '' && element.getAttribute(key).indexOf(attributes[key]) == -1)
) {
match = false;
break;
}
}
if (match) {
return element;
}
}
element = element.parentElement;
}
return null;
}
function refreshToken()
{
var xhr = new XMLHttpRequest();
xhr.open('GET', '?do=token');
xhr.onload = function() {
var token = document.getElementById('token');
token.setAttribute('value', xhr.responseText);
};
xhr.send();
}
/**
* html_entities in JS
*
* @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
*/
function htmlEntities(str)
{
return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
return '&#'+i.charCodeAt(0)+';';
});
}
function activateFirefoxSocial(node) {
var loc = location.href;
var baseURL = loc.substring(0, loc.lastIndexOf("/"));
@ -445,8 +576,11 @@ function activateFirefoxSocial(node) {
* @param currentContinent Current selected continent
* @param reset Set to true to reset the selected value
*/
function hideTimezoneCities(cities, currentContinent, reset = false) {
function hideTimezoneCities(cities, currentContinent) {
var first = true;
if (reset == null) {
reset = false;
}
[].forEach.call(cities, function (option) {
if (option.getAttribute('data-continent') != currentContinent) {
option.className = 'hidden';

82
tpl/default/tag.list.html Normal file
View file

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
{include="includes"}
</head>
<body>
{include="page.header"}
{include="tag.sort"}
<div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
{$countTags=count($tags)}
<h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
<div id="search-tagcloud" class="pure-g">
<div class="pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-1-2">
<form method="GET">
<input type="hidden" name="do" value="taglist">
<input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
{if="!empty($search_tags)"}
value="{$search_tags}"
{/if}
autocomplete="off" data-multiple data-autofirst data-minChars="1"
data-list="{loop="$tags"}{$key}, {/loop}"
>
<button type="submit" class="search-button"><i class="fa fa-search"></i></button>
</form>
</div>
<div class="pure-u-lg-1-4"></div>
</div>
<div id="plugin_zone_start_tagcloud" class="plugin_zone">
{loop="$plugin_start_zone"}
{$value}
{/loop}
</div>
<div id="taglist">
{loop="tags"}
<div class="tag-list-item pure-g" data-tag="{$key}">
<div class="pure-u-1">
{if="isLoggedIn()===true"}
<a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>&nbsp;&nbsp;
<a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
<i class="fa fa-pencil-square-o {$key}"></i>
</a>
{/if}
<a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
<a href="?searchtags={$key|urlencode}" class="tag-link">{$key}</a>
{loop="$value.tag_plugin"}
{$value}
{/loop}
</div>
{if="isLoggedIn()===true"}
<div class="rename-tag-form pure-u-1" style="display:none;">
<input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
<a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
</div>
{/if}
</div>
{/loop}
</div>
<div id="plugin_zone_end_tagcloud" class="plugin_zone">
{loop="$plugin_end_zone"}
{$value}
{/loop}
</div>
</div>
</div>
{include="tag.sort"}
{include="page.footer"}
</body>
</html>

View file

@ -0,0 +1,8 @@
<div class="pure-g">
<div class="pure-u-1 pure-alert pure-alert-success tag-sort">
{'Sort by:'|t}
<a href="?do=tagcloud" title="cloud">{'Cloud'|t}</a> &middot;
<a href="?do=taglist&sort=usage" title="cloud">{'Most used'|t}</a> &middot;
<a href="?do=taglist&sort=alpha" title="cloud">{'Alphabetical'|t}</a>
</div>
</div>