aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/Router.php12
-rw-r--r--application/Utils.php31
-rw-r--r--index.php49
-rw-r--r--tests/UtilsTest.php112
-rw-r--r--tpl/default/changetag.html4
-rw-r--r--tpl/default/css/shaarli.css54
-rw-r--r--tpl/default/js/shaarli.js194
-rw-r--r--tpl/default/page.footer.html3
-rw-r--r--tpl/default/tag.cloud.html (renamed from tpl/default/tagcloud.html)4
-rw-r--r--tpl/default/tag.list.html86
-rw-r--r--tpl/default/tag.sort.html8
-rw-r--r--tpl/vintage/tag.cloud.html (renamed from tpl/vintage/tagcloud.html)0
12 files changed, 540 insertions, 17 deletions
diff --git a/application/Router.php b/application/Router.php
index c9a51912..4df0387c 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -13,6 +13,8 @@ class Router
13 13
14 public static $PAGE_TAGCLOUD = 'tagcloud'; 14 public static $PAGE_TAGCLOUD = 'tagcloud';
15 15
16 public static $PAGE_TAGLIST = 'taglist';
17
16 public static $PAGE_DAILY = 'daily'; 18 public static $PAGE_DAILY = 'daily';
17 19
18 public static $PAGE_FEED_ATOM = 'atom'; 20 public static $PAGE_FEED_ATOM = 'atom';
@@ -45,6 +47,8 @@ class Router
45 47
46 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin'; 48 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
47 49
50 public static $GET_TOKEN = 'token';
51
48 /** 52 /**
49 * Reproducing renderPage() if hell, to avoid regression. 53 * Reproducing renderPage() if hell, to avoid regression.
50 * 54 *
@@ -77,6 +81,10 @@ class Router
77 return self::$PAGE_TAGCLOUD; 81 return self::$PAGE_TAGCLOUD;
78 } 82 }
79 83
84 if (startsWith($query, 'do='. self::$PAGE_TAGLIST)) {
85 return self::$PAGE_TAGLIST;
86 }
87
80 if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) { 88 if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) {
81 return self::$PAGE_OPENSEARCH; 89 return self::$PAGE_OPENSEARCH;
82 } 90 }
@@ -142,6 +150,10 @@ class Router
142 return self::$PAGE_SAVE_PLUGINSADMIN; 150 return self::$PAGE_SAVE_PLUGINSADMIN;
143 } 151 }
144 152
153 if (startsWith($query, 'do='. self::$GET_TOKEN)) {
154 return self::$GET_TOKEN;
155 }
156
145 return self::$PAGE_LINKLIST; 157 return self::$PAGE_LINKLIST;
146 } 158 }
147} 159}
diff --git a/application/Utils.php b/application/Utils.php
index ab463af9..9d0ebc5e 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -435,3 +435,34 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
435 $maxsize = min($size1, $size2); 435 $maxsize = min($size1, $size2);
436 return $format ? human_bytes($maxsize) : $maxsize; 436 return $format ? human_bytes($maxsize) : $maxsize;
437} 437}
438
439/**
440 * Sort the given array alphabetically using php-intl if available.
441 * Case sensitive.
442 *
443 * Note: doesn't support multidimensional arrays
444 *
445 * @param array $data Input array, passed by reference
446 * @param bool $reverse Reverse sort if set to true
447 * @param bool $byKeys Sort the array by keys if set to true, by value otherwise.
448 */
449function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
450{
451 $callback = function($a, $b) use ($reverse) {
452 // Collator is part of PHP intl.
453 if (class_exists('Collator')) {
454 $collator = new Collator(setlocale(LC_COLLATE, 0));
455 if (!intl_is_failure(intl_get_error_code())) {
456 return $collator->compare($a, $b) * ($reverse ? -1 : 1);
457 }
458 }
459
460 return strcasecmp($a, $b) * ($reverse ? -1 : 1);
461 };
462
463 if ($byKeys) {
464 uksort($data, $callback);
465 } else {
466 usort($data, $callback);
467 }
468}
diff --git a/index.php b/index.php
index 40539a04..61b71129 100644
--- a/index.php
+++ b/index.php
@@ -791,7 +791,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
791 if ($targetPage == Router::$PAGE_TAGCLOUD) 791 if ($targetPage == Router::$PAGE_TAGCLOUD)
792 { 792 {
793 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; 793 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
794 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : array(); 794 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
795 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); 795 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
796 796
797 // We sort tags alphabetically, then choose a font size according to count. 797 // We sort tags alphabetically, then choose a font size according to count.
@@ -801,17 +801,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
801 $maxcount = max($maxcount, $value); 801 $maxcount = max($maxcount, $value);
802 } 802 }
803 803
804 // Sort tags alphabetically: case insensitive, support locale if available. 804 alphabetical_sort($tags, true, true);
805 uksort($tags, function($a, $b) {
806 // Collator is part of PHP intl.
807 if (class_exists('Collator')) {
808 $c = new Collator(setlocale(LC_COLLATE, 0));
809 if (!intl_is_failure(intl_get_error_code())) {
810 return $c->compare($a, $b);
811 }
812 }
813 return strcasecmp($a, $b);
814 });
815 805
816 $tagList = array(); 806 $tagList = array();
817 foreach($tags as $key => $value) { 807 foreach($tags as $key => $value) {
@@ -835,7 +825,32 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
835 $PAGE->assign($key, $value); 825 $PAGE->assign($key, $value);
836 } 826 }
837 827
838 $PAGE->renderPage('tagcloud'); 828 $PAGE->renderPage('tag.cloud');
829 exit;
830 }
831
832 // -------- Tag cloud
833 if ($targetPage == Router::$PAGE_TAGLIST)
834 {
835 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
836 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
837 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
838
839 if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
840 alphabetical_sort($tags, false, true);
841 }
842
843 $data = [
844 'search_tags' => implode(' ', $filteringTags),
845 'tags' => $tags,
846 ];
847 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
848
849 foreach ($data as $key => $value) {
850 $PAGE->assign($key, $value);
851 }
852
853 $PAGE->renderPage('tag.list');
839 exit; 854 exit;
840 } 855 }
841 856
@@ -1152,6 +1167,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1152 if ($targetPage == Router::$PAGE_CHANGETAG) 1167 if ($targetPage == Router::$PAGE_CHANGETAG)
1153 { 1168 {
1154 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { 1169 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1170 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
1155 $PAGE->renderPage('changetag'); 1171 $PAGE->renderPage('changetag');
1156 exit; 1172 exit;
1157 } 1173 }
@@ -1582,6 +1598,13 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
1582 exit; 1598 exit;
1583 } 1599 }
1584 1600
1601 // Get a fresh token
1602 if ($targetPage == Router::$GET_TOKEN) {
1603 header('Content-Type:text/plain');
1604 echo getToken($conf);
1605 exit;
1606 }
1607
1585 // -------- Otherwise, simply display search form and links: 1608 // -------- Otherwise, simply display search form and links:
1586 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); 1609 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
1587 exit; 1610 exit;
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index d6a0aad5..3d1aa653 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -417,4 +417,116 @@ class UtilsTest extends PHPUnit_Framework_TestCase
417 $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false)); 417 $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
418 $this->assertEquals('100', get_max_upload_size(100, 100, false)); 418 $this->assertEquals('100', get_max_upload_size(100, 100, false));
419 } 419 }
420
421 /**
422 * Test alphabetical_sort by value, not reversed, with php-intl.
423 */
424 public function testAlphabeticalSortByValue()
425 {
426 $arr = [
427 'zZz',
428 'éee',
429 'éae',
430 'eee',
431 'A',
432 'a',
433 'zzz',
434 ];
435 $expected = [
436 'a',
437 'A',
438 'éae',
439 'eee',
440 'éee',
441 'zzz',
442 'zZz',
443 ];
444
445 alphabetical_sort($arr);
446 $this->assertEquals($expected, $arr);
447 }
448
449 /**
450 * Test alphabetical_sort by value, reversed, with php-intl.
451 */
452 public function testAlphabeticalSortByValueReversed()
453 {
454 $arr = [
455 'zZz',
456 'éee',
457 'éae',
458 'eee',
459 'A',
460 'a',
461 'zzz',
462 ];
463 $expected = [
464 'zZz',
465 'zzz',
466 'éee',
467 'eee',
468 'éae',
469 'A',
470 'a',
471 ];
472
473 alphabetical_sort($arr, true);
474 $this->assertEquals($expected, $arr);
475 }
476
477 /**
478 * Test alphabetical_sort by keys, not reversed, with php-intl.
479 */
480 public function testAlphabeticalSortByKeys()
481 {
482 $arr = [
483 'zZz' => true,
484 'éee' => true,
485 'éae' => true,
486 'eee' => true,
487 'A' => true,
488 'a' => true,
489 'zzz' => true,
490 ];
491 $expected = [
492 'a' => true,
493 'A' => true,
494 'éae' => true,
495 'eee' => true,
496 'éee' => true,
497 'zzz' => true,
498 'zZz' => true,
499 ];
500
501 alphabetical_sort($arr, true, true);
502 $this->assertEquals($expected, $arr);
503 }
504
505 /**
506 * Test alphabetical_sort by keys, reversed, with php-intl.
507 */
508 public function testAlphabeticalSortByKeysReversed()
509 {
510 $arr = [
511 'zZz' => true,
512 'éee' => true,
513 'éae' => true,
514 'eee' => true,
515 'A' => true,
516 'a' => true,
517 'zzz' => true,
518 ];
519 $expected = [
520 'zZz' => true,
521 'zzz' => true,
522 'éee' => true,
523 'eee' => true,
524 'éae' => true,
525 'A' => true,
526 'a' => true,
527 ];
528
529 alphabetical_sort($arr, true, true);
530 $this->assertEquals($expected, $arr);
531 }
420} 532}
diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html
index 8d263a16..49dd20d9 100644
--- a/tpl/default/changetag.html
+++ b/tpl/default/changetag.html
@@ -11,7 +11,7 @@
11 <h2 class="window-title">{"Manage tags"|t}</h2> 11 <h2 class="window-title">{"Manage tags"|t}</h2>
12 <form method="POST" action="#" name="changetag" id="changetag"> 12 <form method="POST" action="#" name="changetag" id="changetag">
13 <div> 13 <div>
14 <input type="text" name="fromtag" placeholder="{'Tag'|t}" 14 <input type="text" name="fromtag" placeholder="{'Tag'|t}" value="{$fromtag}"
15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1"> 15 list="tagsList" autocomplete="off" class="awesomplete autofocus" data-minChars="1">
16 <datalist id="tagsList"> 16 <datalist id="tagsList">
17 {loop="$tags"}<option>{$key}</option>{/loop} 17 {loop="$tags"}<option>{$key}</option>{/loop}
@@ -31,6 +31,8 @@
31 <input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete"> 31 <input type="submit" value="{'Delete'|t}" name="deletetag" class="button button-red confirm-delete">
32 </div> 32 </div>
33 </form> 33 </form>
34
35 <p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p>
34 </div> 36 </div>
35</div> 37</div>
36{include="page.footer"} 38{include="page.footer"}
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css
index 4415a1b7..28920648 100644
--- a/tpl/default/css/shaarli.css
+++ b/tpl/default/css/shaarli.css
@@ -751,10 +751,11 @@ body, .pure-g [class*="pure-u"] {
751.page-form a { 751.page-form a {
752 color: #1b926c; 752 color: #1b926c;
753 font-weight: bold; 753 font-weight: bold;
754 text-decoration: none;
754} 755}
755 756
756.page-form p { 757.page-form p {
757 padding: 0 10px; 758 padding: 5px 10px;
758 margin: 0; 759 margin: 0;
759} 760}
760 761
@@ -1070,7 +1071,7 @@ form[name="linkform"].page-form {
1070} 1071}
1071 1072
1072#cloudtag, #cloudtag a { 1073#cloudtag, #cloudtag a {
1073 color: #000; 1074 color: #252525;
1074 text-decoration: none; 1075 text-decoration: none;
1075} 1076}
1076 1077
@@ -1079,6 +1080,42 @@ form[name="linkform"].page-form {
1079} 1080}
1080 1081
1081/** 1082/**
1083 * TAG LIST
1084 */
1085#taglist {
1086 padding: 0 10px;
1087}
1088
1089#taglist a {
1090 color: #252525;
1091 text-decoration: none;
1092}
1093
1094#taglist .count {
1095 display: inline-block;
1096 width: 35px;
1097 text-align: right;
1098 color: #7f7f7f;
1099}
1100
1101#taglist .rename-tag-form {
1102 display: none;
1103}
1104
1105#taglist .delete-tag {
1106 color: #ac2925;
1107 display: none;
1108}
1109
1110#taglist .rename-tag {
1111 color: #0b5ea6;
1112}
1113
1114#taglist .validate-rename-tag {
1115 color: #1b926c;
1116}
1117
1118/**
1082 * Picture wall CSS 1119 * Picture wall CSS
1083 */ 1120 */
1084#picwall_container { 1121#picwall_container {
@@ -1227,3 +1264,16 @@ form[name="linkform"].page-form {
1227.pure-button { 1264.pure-button {
1228 -moz-user-select: auto; 1265 -moz-user-select: auto;
1229} 1266}
1267
1268.tag-sort {
1269 margin-top: 30px;
1270 text-align: center;
1271}
1272
1273.tag-sort a {
1274 display: inline-block;
1275 margin: 0 15px;
1276 color: white;
1277 text-decoration: none;
1278 font-weight: bold;
1279}
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index ceb1d1b8..4ebb7815 100644
--- a/tpl/default/js/shaarli.js
+++ b/tpl/default/js/shaarli.js
@@ -412,8 +412,197 @@ window.onload = function () {
412 } 412 }
413 }); 413 });
414 } 414 }
415
416 /**
417 * Tag list operations
418 *
419 * TODO: support error code in the backend for AJAX requests
420 */
421 var existingTags = document.querySelector('input[name="taglist"]').value.split(' ');
422 var awesomepletes = [];
423
424 // Display/Hide rename form
425 var renameTagButtons = document.querySelectorAll('.rename-tag');
426 [].forEach.call(renameTagButtons, function(rename) {
427 rename.addEventListener('click', function(event) {
428 event.preventDefault();
429 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
430 var form = block.querySelector('.rename-tag-form');
431 if (form.style.display == 'none' || form.style.display == '') {
432 form.style.display = 'block';
433 } else {
434 form.style.display = 'none';
435 }
436 block.querySelector('input').focus();
437 });
438 });
439
440 // Rename a tag with an AJAX request
441 var renameTagSubmits = document.querySelectorAll('.validate-rename-tag');
442 [].forEach.call(renameTagSubmits, function(rename) {
443 rename.addEventListener('click', function(event) {
444 event.preventDefault();
445 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
446 var input = block.querySelector('.rename-tag-input');
447 var totag = input.value.replace('/"/g', '\\"');
448 if (totag.trim() == '') {
449 return;
450 }
451 var fromtag = block.getAttribute('data-tag');
452 var token = document.getElementById('token').value;
453
454 xhr = new XMLHttpRequest();
455 xhr.open('POST', '?do=changetag');
456 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
457 xhr.onload = function() {
458 if (xhr.status !== 200) {
459 alert('An error occurred. Return code: '+ xhr.status);
460 location.reload();
461 } else {
462 block.setAttribute('data-tag', totag);
463 input.setAttribute('name', totag);
464 input.setAttribute('value', totag);
465 findParent(input, 'div', {'class': 'rename-tag-form'}).style.display = 'none';
466 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
467 block.querySelector('a.tag-link').setAttribute('href', '?searchtags='+ encodeURIComponent(totag));
468 block.querySelector('a.rename-tag').setAttribute('href', '?do=changetag&fromtag='+ encodeURIComponent(totag));
469
470 // Refresh awesomplete values
471 for (var key in existingTags) {
472 if (existingTags[key] == fromtag) {
473 existingTags[key] = totag;
474 }
475 }
476 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
477 }
478 };
479 xhr.send('renametag=1&fromtag='+ encodeURIComponent(fromtag) +'&totag='+ encodeURIComponent(totag) +'&token='+ token);
480 refreshToken();
481 });
482 });
483
484 // Validate input with enter key
485 var renameTagInputs = document.querySelectorAll('.rename-tag-input');
486 [].forEach.call(renameTagInputs, function(rename) {
487
488 rename.addEventListener('keypress', function(event) {
489 if (event.keyCode === 13) { // enter
490 findParent(event.target, 'div', {'class': 'tag-list-item'}).querySelector('.validate-rename-tag').click();
491 }
492 });
493 });
494
495 // Delete a tag with an AJAX query (alert popup confirmation)
496 var deleteTagButtons = document.querySelectorAll('.delete-tag');
497 [].forEach.call(deleteTagButtons, function(rename) {
498 rename.style.display = 'inline';
499 rename.addEventListener('click', function(event) {
500 event.preventDefault();
501 var block = findParent(event.target, 'div', {'class': 'tag-list-item'});
502 var tag = block.getAttribute('data-tag');
503 var token = document.getElementById('token').value;
504
505 if (confirm('Are you sure you want to delete the tag "'+ tag +'"?')) {
506 xhr = new XMLHttpRequest();
507 xhr.open('POST', '?do=changetag');
508 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
509 xhr.onload = function() {
510 block.remove();
511 };
512 xhr.send(encodeURI('deletetag=1&fromtag='+ tag +'&token='+ token));
513 refreshToken();
514 }
515 });
516 });
517
518 updateAwesompleteList('.rename-tag-input', document.querySelector('input[name="taglist"]').value.split(' '), awesomepletes);
415}; 519};
416 520
521/**
522 * Find a parent element according to its tag and its attributes
523 *
524 * @param element Element where to start the search
525 * @param tagName Expected parent tag name
526 * @param attributes Associative array of expected attributes (name=>value).
527 *
528 * @returns Found element or null.
529 */
530function findParent(element, tagName, attributes)
531{
532 while (element) {
533 if (element.tagName.toLowerCase() == tagName) {
534 var match = true;
535 for (var key in attributes) {
536 if (! element.hasAttribute(key)
537 || (attributes[key] != '' && element.getAttribute(key).indexOf(attributes[key]) == -1)
538 ) {
539 match = false;
540 break;
541 }
542 }
543
544 if (match) {
545 return element;
546 }
547 }
548 element = element.parentElement;
549 }
550 return null;
551}
552
553/**
554 * Ajax request to refresh the CSRF token.
555 */
556function refreshToken()
557{
558 var xhr = new XMLHttpRequest();
559 xhr.open('GET', '?do=token');
560 xhr.onload = function() {
561 var token = document.getElementById('token');
562 token.setAttribute('value', xhr.responseText);
563 };
564 xhr.send();
565}
566
567/**
568 * Update awesomplete list of tag for all elements matching the given selector
569 *
570 * @param selector CSS selector
571 * @param tags Array of tags
572 * @param instances List of existing awesomplete instances
573 */
574function updateAwesompleteList(selector, tags, instances)
575{
576 // First load: create Awesomplete instances
577 if (instances.length == 0) {
578 var elements = document.querySelectorAll(selector);
579 [].forEach.call(elements, function (element) {
580 instances.push(new Awesomplete(
581 element,
582 {'list': tags}
583 ));
584 });
585 } else {
586 // Update awesomplete tag list
587 for (var key in instances) {
588 instances[key].list = tags;
589 }
590 }
591 return instances;
592}
593
594/**
595 * html_entities in JS
596 *
597 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
598 */
599function htmlEntities(str)
600{
601 return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
602 return '&#'+i.charCodeAt(0)+';';
603 });
604}
605
417function activateFirefoxSocial(node) { 606function activateFirefoxSocial(node) {
418 var loc = location.href; 607 var loc = location.href;
419 var baseURL = loc.substring(0, loc.lastIndexOf("/")); 608 var baseURL = loc.substring(0, loc.lastIndexOf("/"));
@@ -445,8 +634,11 @@ function activateFirefoxSocial(node) {
445 * @param currentContinent Current selected continent 634 * @param currentContinent Current selected continent
446 * @param reset Set to true to reset the selected value 635 * @param reset Set to true to reset the selected value
447 */ 636 */
448function hideTimezoneCities(cities, currentContinent, reset = false) { 637function hideTimezoneCities(cities, currentContinent) {
449 var first = true; 638 var first = true;
639 if (reset == null) {
640 reset = false;
641 }
450 [].forEach.call(cities, function (option) { 642 [].forEach.call(cities, function (option) {
451 if (option.getAttribute('data-continent') != currentContinent) { 643 if (option.getAttribute('data-continent') != currentContinent) {
452 option.className = 'hidden'; 644 option.className = 'hidden';
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html
index 77fc65dd..02fc7642 100644
--- a/tpl/default/page.footer.html
+++ b/tpl/default/page.footer.html
@@ -16,6 +16,9 @@
16 </div> 16 </div>
17 <div class="pure-u-2-24"></div> 17 <div class="pure-u-2-24"></div>
18</div> 18</div>
19
20<input type="hidden" name="token" value="{$token}" id="token" />
21
19{loop="$plugins_footer.endofpage"} 22{loop="$plugins_footer.endofpage"}
20 {$value} 23 {$value}
21{/loop} 24{/loop}
diff --git a/tpl/default/tagcloud.html b/tpl/default/tag.cloud.html
index efe6e937..59aa2ee0 100644
--- a/tpl/default/tagcloud.html
+++ b/tpl/default/tag.cloud.html
@@ -6,6 +6,8 @@
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8 8
9{include="tag.sort"}
10
9<div class="pure-g"> 11<div class="pure-g">
10 <div class="pure-u-lg-1-6 pure-u-1-24"></div> 12 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
11 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor"> 13 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
@@ -54,6 +56,8 @@
54 </div> 56 </div>
55</div> 57</div>
56 58
59{include="tag.sort"}
60
57{include="page.footer"} 61{include="page.footer"}
58</body> 62</body>
59</html> 63</html>
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html
new file mode 100644
index 00000000..62e2e7c6
--- /dev/null
+++ b/tpl/default/tag.list.html
@@ -0,0 +1,86 @@
1<!DOCTYPE html>
2<html>
3<head>
4 {include="includes"}
5</head>
6<body>
7{include="page.header"}
8
9{include="tag.sort"}
10
11<div class="pure-g">
12 <div class="pure-u-lg-1-6 pure-u-1-24"></div>
13 <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-visitor">
14 {$countTags=count($tags)}
15 <h2 class="window-title">{'Tag list'|t} - {$countTags} {'tags'|t}</h2>
16
17 <div id="search-tagcloud" class="pure-g">
18 <div class="pure-u-lg-1-4"></div>
19 <div class="pure-u-1 pure-u-lg-1-2">
20 <form method="GET">
21 <input type="hidden" name="do" value="taglist">
22 <input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
23 {if="!empty($search_tags)"}
24 value="{$search_tags}"
25 {/if}
26 autocomplete="off" data-multiple data-autofirst data-minChars="1"
27 data-list="{loop="$tags"}{$key}, {/loop}"
28 >
29 <button type="submit" class="search-button"><i class="fa fa-search"></i></button>
30 </form>
31 </div>
32 <div class="pure-u-lg-1-4"></div>
33 </div>
34
35 <div id="plugin_zone_start_tagcloud" class="plugin_zone">
36 {loop="$plugin_start_zone"}
37 {$value}
38 {/loop}
39 </div>
40
41 <div id="taglist">
42 {loop="tags"}
43 <div class="tag-list-item pure-g" data-tag="{$key}">
44 <div class="pure-u-1">
45 {if="isLoggedIn()===true"}
46 <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>&nbsp;&nbsp;
47 <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
48 <i class="fa fa-pencil-square-o {$key}"></i>
49 </a>
50 {/if}
51
52 <a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
53 <a href="?searchtags={$key|urlencode}" class="tag-link">{$key}</a>
54
55 {loop="$value.tag_plugin"}
56 {$value}
57 {/loop}
58 </div>
59 {if="isLoggedIn()===true"}
60 <div class="rename-tag-form pure-u-1">
61 <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
62 <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
63 </div>
64 {/if}
65 </div>
66 {/loop}
67 </div>
68
69 <div id="plugin_zone_end_tagcloud" class="plugin_zone">
70 {loop="$plugin_end_zone"}
71 {$value}
72 {/loop}
73 </div>
74 </div>
75</div>
76
77{if="isLoggedIn()===true"}
78 <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
79{/if}
80
81{include="tag.sort"}
82
83{include="page.footer"}
84</body>
85</html>
86
diff --git a/tpl/default/tag.sort.html b/tpl/default/tag.sort.html
new file mode 100644
index 00000000..89acda0d
--- /dev/null
+++ b/tpl/default/tag.sort.html
@@ -0,0 +1,8 @@
1<div class="pure-g">
2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
3 {'Sort by:'|t}
4 <a href="?do=tagcloud" title="cloud">{'Cloud'|t}</a> &middot;
5 <a href="?do=taglist&sort=usage" title="cloud">{'Most used'|t}</a> &middot;
6 <a href="?do=taglist&sort=alpha" title="cloud">{'Alphabetical'|t}</a>
7 </div>
8</div> \ No newline at end of file
diff --git a/tpl/vintage/tagcloud.html b/tpl/vintage/tag.cloud.html
index d93bf4f9..d93bf4f9 100644
--- a/tpl/vintage/tagcloud.html
+++ b/tpl/vintage/tag.cloud.html