Merge pull request #835 from ArthurHoaro/feature/tag-cloud
Adds a taglist view with edit/delete buttons
This commit is contained in:
commit
81a91579ba
12 changed files with 540 additions and 17 deletions
|
@ -13,6 +13,8 @@ class Router
|
||||||
|
|
||||||
public static $PAGE_TAGCLOUD = 'tagcloud';
|
public static $PAGE_TAGCLOUD = 'tagcloud';
|
||||||
|
|
||||||
|
public static $PAGE_TAGLIST = 'taglist';
|
||||||
|
|
||||||
public static $PAGE_DAILY = 'daily';
|
public static $PAGE_DAILY = 'daily';
|
||||||
|
|
||||||
public static $PAGE_FEED_ATOM = 'atom';
|
public static $PAGE_FEED_ATOM = 'atom';
|
||||||
|
@ -45,6 +47,8 @@ class Router
|
||||||
|
|
||||||
public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
|
public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
|
||||||
|
|
||||||
|
public static $GET_TOKEN = 'token';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reproducing renderPage() if hell, to avoid regression.
|
* Reproducing renderPage() if hell, to avoid regression.
|
||||||
*
|
*
|
||||||
|
@ -77,6 +81,10 @@ public static function findPage($query, $get, $loggedIn)
|
||||||
return self::$PAGE_TAGCLOUD;
|
return self::$PAGE_TAGCLOUD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startsWith($query, 'do='. self::$PAGE_TAGLIST)) {
|
||||||
|
return self::$PAGE_TAGLIST;
|
||||||
|
}
|
||||||
|
|
||||||
if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) {
|
if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) {
|
||||||
return self::$PAGE_OPENSEARCH;
|
return self::$PAGE_OPENSEARCH;
|
||||||
}
|
}
|
||||||
|
@ -142,6 +150,10 @@ public static function findPage($query, $get, $loggedIn)
|
||||||
return self::$PAGE_SAVE_PLUGINSADMIN;
|
return self::$PAGE_SAVE_PLUGINSADMIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (startsWith($query, 'do='. self::$GET_TOKEN)) {
|
||||||
|
return self::$GET_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
return self::$PAGE_LINKLIST;
|
return self::$PAGE_LINKLIST;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -435,3 +435,34 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
|
||||||
$maxsize = min($size1, $size2);
|
$maxsize = min($size1, $size2);
|
||||||
return $format ? human_bytes($maxsize) : $maxsize;
|
return $format ? human_bytes($maxsize) : $maxsize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort the given array alphabetically using php-intl if available.
|
||||||
|
* Case sensitive.
|
||||||
|
*
|
||||||
|
* Note: doesn't support multidimensional arrays
|
||||||
|
*
|
||||||
|
* @param array $data Input array, passed by reference
|
||||||
|
* @param bool $reverse Reverse sort if set to true
|
||||||
|
* @param bool $byKeys Sort the array by keys if set to true, by value otherwise.
|
||||||
|
*/
|
||||||
|
function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
|
||||||
|
{
|
||||||
|
$callback = function($a, $b) use ($reverse) {
|
||||||
|
// Collator is part of PHP intl.
|
||||||
|
if (class_exists('Collator')) {
|
||||||
|
$collator = new Collator(setlocale(LC_COLLATE, 0));
|
||||||
|
if (!intl_is_failure(intl_get_error_code())) {
|
||||||
|
return $collator->compare($a, $b) * ($reverse ? -1 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strcasecmp($a, $b) * ($reverse ? -1 : 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($byKeys) {
|
||||||
|
uksort($data, $callback);
|
||||||
|
} else {
|
||||||
|
usort($data, $callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
49
index.php
49
index.php
|
@ -791,7 +791,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
|
||||||
if ($targetPage == Router::$PAGE_TAGCLOUD)
|
if ($targetPage == Router::$PAGE_TAGCLOUD)
|
||||||
{
|
{
|
||||||
$visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
|
$visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
|
||||||
$filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : array();
|
$filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
|
||||||
$tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
|
$tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
|
||||||
|
|
||||||
// We sort tags alphabetically, then choose a font size according to count.
|
// We sort tags alphabetically, then choose a font size according to count.
|
||||||
|
@ -801,17 +801,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
|
||||||
$maxcount = max($maxcount, $value);
|
$maxcount = max($maxcount, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort tags alphabetically: case insensitive, support locale if available.
|
alphabetical_sort($tags, true, true);
|
||||||
uksort($tags, function($a, $b) {
|
|
||||||
// Collator is part of PHP intl.
|
|
||||||
if (class_exists('Collator')) {
|
|
||||||
$c = new Collator(setlocale(LC_COLLATE, 0));
|
|
||||||
if (!intl_is_failure(intl_get_error_code())) {
|
|
||||||
return $c->compare($a, $b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strcasecmp($a, $b);
|
|
||||||
});
|
|
||||||
|
|
||||||
$tagList = array();
|
$tagList = array();
|
||||||
foreach($tags as $key => $value) {
|
foreach($tags as $key => $value) {
|
||||||
|
@ -835,7 +825,32 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
|
||||||
$PAGE->assign($key, $value);
|
$PAGE->assign($key, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$PAGE->renderPage('tagcloud');
|
$PAGE->renderPage('tag.cloud');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Tag cloud
|
||||||
|
if ($targetPage == Router::$PAGE_TAGLIST)
|
||||||
|
{
|
||||||
|
$visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
|
||||||
|
$filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
|
||||||
|
$tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
|
||||||
|
|
||||||
|
if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
|
||||||
|
alphabetical_sort($tags, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'search_tags' => implode(' ', $filteringTags),
|
||||||
|
'tags' => $tags,
|
||||||
|
];
|
||||||
|
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
|
||||||
|
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
$PAGE->assign($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$PAGE->renderPage('tag.list');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1152,6 +1167,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
|
||||||
if ($targetPage == Router::$PAGE_CHANGETAG)
|
if ($targetPage == Router::$PAGE_CHANGETAG)
|
||||||
{
|
{
|
||||||
if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
|
if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
|
||||||
|
$PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
|
||||||
$PAGE->renderPage('changetag');
|
$PAGE->renderPage('changetag');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
@ -1582,6 +1598,13 @@ function($a, $b) { return $a['order'] - $b['order']; }
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a fresh token
|
||||||
|
if ($targetPage == Router::$GET_TOKEN) {
|
||||||
|
header('Content-Type:text/plain');
|
||||||
|
echo getToken($conf);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// -------- Otherwise, simply display search form and links:
|
// -------- Otherwise, simply display search form and links:
|
||||||
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
|
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
|
||||||
exit;
|
exit;
|
||||||
|
|
|
@ -417,4 +417,116 @@ public function testGetMaxUploadSizeRaw()
|
||||||
$this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
|
$this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
|
||||||
$this->assertEquals('100', get_max_upload_size(100, 100, false));
|
$this->assertEquals('100', get_max_upload_size(100, 100, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test alphabetical_sort by value, not reversed, with php-intl.
|
||||||
|
*/
|
||||||
|
public function testAlphabeticalSortByValue()
|
||||||
|
{
|
||||||
|
$arr = [
|
||||||
|
'zZz',
|
||||||
|
'éee',
|
||||||
|
'éae',
|
||||||
|
'eee',
|
||||||
|
'A',
|
||||||
|
'a',
|
||||||
|
'zzz',
|
||||||
|
];
|
||||||
|
$expected = [
|
||||||
|
'a',
|
||||||
|
'A',
|
||||||
|
'éae',
|
||||||
|
'eee',
|
||||||
|
'éee',
|
||||||
|
'zzz',
|
||||||
|
'zZz',
|
||||||
|
];
|
||||||
|
|
||||||
|
alphabetical_sort($arr);
|
||||||
|
$this->assertEquals($expected, $arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test alphabetical_sort by value, reversed, with php-intl.
|
||||||
|
*/
|
||||||
|
public function testAlphabeticalSortByValueReversed()
|
||||||
|
{
|
||||||
|
$arr = [
|
||||||
|
'zZz',
|
||||||
|
'éee',
|
||||||
|
'éae',
|
||||||
|
'eee',
|
||||||
|
'A',
|
||||||
|
'a',
|
||||||
|
'zzz',
|
||||||
|
];
|
||||||
|
$expected = [
|
||||||
|
'zZz',
|
||||||
|
'zzz',
|
||||||
|
'éee',
|
||||||
|
'eee',
|
||||||
|
'éae',
|
||||||
|
'A',
|
||||||
|
'a',
|
||||||
|
];
|
||||||
|
|
||||||
|
alphabetical_sort($arr, true);
|
||||||
|
$this->assertEquals($expected, $arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test alphabetical_sort by keys, not reversed, with php-intl.
|
||||||
|
*/
|
||||||
|
public function testAlphabeticalSortByKeys()
|
||||||
|
{
|
||||||
|
$arr = [
|
||||||
|
'zZz' => true,
|
||||||
|
'éee' => true,
|
||||||
|
'éae' => true,
|
||||||
|
'eee' => true,
|
||||||
|
'A' => true,
|
||||||
|
'a' => true,
|
||||||
|
'zzz' => true,
|
||||||
|
];
|
||||||
|
$expected = [
|
||||||
|
'a' => true,
|
||||||
|
'A' => true,
|
||||||
|
'éae' => true,
|
||||||
|
'eee' => true,
|
||||||
|
'éee' => true,
|
||||||
|
'zzz' => true,
|
||||||
|
'zZz' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
alphabetical_sort($arr, true, true);
|
||||||
|
$this->assertEquals($expected, $arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test alphabetical_sort by keys, reversed, with php-intl.
|
||||||
|
*/
|
||||||
|
public function testAlphabeticalSortByKeysReversed()
|
||||||
|
{
|
||||||
|
$arr = [
|
||||||
|
'zZz' => true,
|
||||||
|
'éee' => true,
|
||||||
|
'éae' => true,
|
||||||
|
'eee' => true,
|
||||||
|
'A' => true,
|
||||||
|
'a' => true,
|
||||||
|
'zzz' => true,
|
||||||
|
];
|
||||||
|
$expected = [
|
||||||
|
'zZz' => true,
|
||||||
|
'zzz' => true,
|
||||||
|
'éee' => true,
|
||||||
|
'eee' => true,
|
||||||
|
'éae' => true,
|
||||||
|
'A' => true,
|
||||||
|
'a' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
alphabetical_sort($arr, true, true);
|
||||||
|
$this->assertEquals($expected, $arr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<h2 class="window-title">{"Manage tags"|t}</h2>
|
<h2 class="window-title">{"Manage tags"|t}</h2>
|
||||||
<form method="POST" action="#" name="changetag" id="changetag">
|
<form method="POST" action="#" name="changetag" id="changetag">
|
||||||
<div>
|
<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">
|
list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1">
|
||||||
<datalist id="tagsList">
|
<datalist id="tagsList">
|
||||||
{loop="$tags"}<option>{$key}</option>{/loop}
|
{loop="$tags"}<option>{$key}</option>{/loop}
|
||||||
|
@ -31,6 +31,8 @@ <h2 class="window-title">{"Manage tags"|t}</h2>
|
||||||
<input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete">
|
<input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{include="page.footer"}
|
{include="page.footer"}
|
||||||
|
|
|
@ -751,10 +751,11 @@ body, .pure-g [class*="pure-u"] {
|
||||||
.page-form a {
|
.page-form a {
|
||||||
color: #1b926c;
|
color: #1b926c;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-form p {
|
.page-form p {
|
||||||
padding: 0 10px;
|
padding: 5px 10px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1070,7 +1071,7 @@ form[name="linkform"].page-form {
|
||||||
}
|
}
|
||||||
|
|
||||||
#cloudtag, #cloudtag a {
|
#cloudtag, #cloudtag a {
|
||||||
color: #000;
|
color: #252525;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1078,6 +1079,42 @@ form[name="linkform"].page-form {
|
||||||
color: #7f7f7f;
|
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 .rename-tag-form {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#taglist .delete-tag {
|
||||||
|
color: #ac2925;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#taglist .rename-tag {
|
||||||
|
color: #0b5ea6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#taglist .validate-rename-tag {
|
||||||
|
color: #1b926c;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Picture wall CSS
|
* Picture wall CSS
|
||||||
*/
|
*/
|
||||||
|
@ -1227,3 +1264,16 @@ form[name="linkform"].page-form {
|
||||||
.pure-button {
|
.pure-button {
|
||||||
-moz-user-select: auto;
|
-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;
|
||||||
|
}
|
||||||
|
|
|
@ -412,8 +412,197 @@ window.onload = function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag list operations
|
||||||
|
*
|
||||||
|
* TODO: support error code in the backend for AJAX requests
|
||||||
|
*/
|
||||||
|
var existingTags = document.querySelector('input[name="taglist"]').value.split(' ');
|
||||||
|
var awesomepletes = [];
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
if (form.style.display == 'none' || form.style.display == '') {
|
||||||
|
form.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
form.style.display = 'none';
|
||||||
|
}
|
||||||
|
block.querySelector('input').focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
findParent(input, 'div', {'class': 'rename-tag-form'}).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));
|
||||||
|
|
||||||
|
// Refresh awesomplete values
|
||||||
|
for (var key in existingTags) {
|
||||||
|
if (existingTags[key] == fromtag) {
|
||||||
|
existingTags[key] = totag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateAwesompleteList('.rename-tag-input', document.querySelector('input[name="taglist"]').value.split(' '), awesomepletes);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a parent element according to its tag and its attributes
|
||||||
|
*
|
||||||
|
* @param element Element where to start the search
|
||||||
|
* @param tagName Expected parent tag name
|
||||||
|
* @param attributes Associative array of expected attributes (name=>value).
|
||||||
|
*
|
||||||
|
* @returns Found element or null.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax request to refresh the CSRF token.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update awesomplete list of tag for all elements matching the given selector
|
||||||
|
*
|
||||||
|
* @param selector CSS selector
|
||||||
|
* @param tags Array of tags
|
||||||
|
* @param instances List of existing awesomplete instances
|
||||||
|
*/
|
||||||
|
function updateAwesompleteList(selector, tags, instances)
|
||||||
|
{
|
||||||
|
// First load: create Awesomplete instances
|
||||||
|
if (instances.length == 0) {
|
||||||
|
var elements = document.querySelectorAll(selector);
|
||||||
|
[].forEach.call(elements, function (element) {
|
||||||
|
instances.push(new Awesomplete(
|
||||||
|
element,
|
||||||
|
{'list': tags}
|
||||||
|
));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Update awesomplete tag list
|
||||||
|
for (var key in instances) {
|
||||||
|
instances[key].list = tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
function activateFirefoxSocial(node) {
|
||||||
var loc = location.href;
|
var loc = location.href;
|
||||||
var baseURL = loc.substring(0, loc.lastIndexOf("/"));
|
var baseURL = loc.substring(0, loc.lastIndexOf("/"));
|
||||||
|
@ -445,8 +634,11 @@ function activateFirefoxSocial(node) {
|
||||||
* @param currentContinent Current selected continent
|
* @param currentContinent Current selected continent
|
||||||
* @param reset Set to true to reset the selected value
|
* @param reset Set to true to reset the selected value
|
||||||
*/
|
*/
|
||||||
function hideTimezoneCities(cities, currentContinent, reset = false) {
|
function hideTimezoneCities(cities, currentContinent) {
|
||||||
var first = true;
|
var first = true;
|
||||||
|
if (reset == null) {
|
||||||
|
reset = false;
|
||||||
|
}
|
||||||
[].forEach.call(cities, function (option) {
|
[].forEach.call(cities, function (option) {
|
||||||
if (option.getAttribute('data-continent') != currentContinent) {
|
if (option.getAttribute('data-continent') != currentContinent) {
|
||||||
option.className = 'hidden';
|
option.className = 'hidden';
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-2-24"></div>
|
<div class="pure-u-2-24"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="token" value="{$token}" id="token" />
|
||||||
|
|
||||||
{loop="$plugins_footer.endofpage"}
|
{loop="$plugins_footer.endofpage"}
|
||||||
{$value}
|
{$value}
|
||||||
{/loop}
|
{/loop}
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
<body>
|
<body>
|
||||||
{include="page.header"}
|
{include="page.header"}
|
||||||
|
|
||||||
|
{include="tag.sort"}
|
||||||
|
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<div class="pure-u-lg-1-6 pure-u-1-24"></div>
|
<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">
|
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
|
||||||
|
@ -54,6 +56,8 @@ <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{include="tag.sort"}
|
||||||
|
|
||||||
{include="page.footer"}
|
{include="page.footer"}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
86
tpl/default/tag.list.html
Normal file
86
tpl/default/tag.list.html
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<!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>
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{if="isLoggedIn()===true"}
|
||||||
|
<input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{include="tag.sort"}
|
||||||
|
|
||||||
|
{include="page.footer"}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
8
tpl/default/tag.sort.html
Normal file
8
tpl/default/tag.sort.html
Normal 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> ·
|
||||||
|
<a href="?do=taglist&sort=usage" title="cloud">{'Most used'|t}</a> ·
|
||||||
|
<a href="?do=taglist&sort=alpha" title="cloud">{'Alphabetical'|t}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
Loading…
Reference in a new issue