public static $PAGE_TAGCLOUD = 'tagcloud';
+ public static $PAGE_TAGLIST = 'taglist';
+
public static $PAGE_DAILY = 'daily';
public static $PAGE_FEED_ATOM = 'atom';
return self::$PAGE_TAGCLOUD;
}
+ if (startsWith($query, 'do='. self::$PAGE_TAGLIST)) {
+ return self::$PAGE_TAGLIST;
+ }
+
if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) {
return self::$PAGE_OPENSEARCH;
}
$maxsize = min($size1, $size2);
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);
+ }
+}
if ($targetPage == Router::$PAGE_TAGCLOUD)
{
$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);
// We sort tags alphabetically, then choose a font size according to count.
$maxcount = max($maxcount, $value);
}
- // Sort tags alphabetically: case insensitive, support locale if available.
- 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);
- });
+ alphabetical_sort($tags, true, true);
$tagList = array();
foreach($tags as $key => $value) {
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;
+ }
+
// Daily page.
if ($targetPage == Router::$PAGE_DAILY) {
showDaily($PAGE, $LINKSDB, $conf, $pluginManager);
if ($targetPage == Router::$PAGE_CHANGETAG)
{
if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
+ $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
$PAGE->renderPage('changetag');
exit;
}
$this->assertEquals('1048576', get_max_upload_size('1m', '2m', 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);
+ }
}
<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}
<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"}
.page-form a {
color: #1b926c;
font-weight: bold;
+ text-decoration: none;
}
.page-form p {
- padding: 0 10px;
+ padding: 5px 10px;
margin: 0;
}
}
#cloudtag, #cloudtag a {
- color: #000;
+ color: #252525;
text-decoration: none;
}
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
*/
.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;
+}
}
});
}
+
+ /**
+ * 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("/"));
* @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';
--- /dev/null
+<!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" 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>
+
--- /dev/null
+<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>
\ No newline at end of file