diff options
author | ArthurHoaro <arthur@hoa.ro> | 2017-05-25 15:28:26 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-25 15:28:26 +0200 |
commit | 81a91579ba8eb7b5a203722baf927a3653b5cb94 (patch) | |
tree | 228456f9b4d95d03e97fc9bc1b5004aa599f95ee | |
parent | 8b27824338eb445d69730c9b05f05b131ccea52f (diff) | |
parent | 82e3bb5f06dc531ee1080a0313833791a1c1f3c7 (diff) | |
download | Shaarli-81a91579ba8eb7b5a203722baf927a3653b5cb94.tar.gz Shaarli-81a91579ba8eb7b5a203722baf927a3653b5cb94.tar.zst Shaarli-81a91579ba8eb7b5a203722baf927a3653b5cb94.zip |
Merge pull request #835 from ArthurHoaro/feature/tag-cloud
Adds a taglist view with edit/delete buttons
-rw-r--r-- | application/Router.php | 12 | ||||
-rw-r--r-- | application/Utils.php | 31 | ||||
-rw-r--r-- | index.php | 49 | ||||
-rw-r--r-- | tests/UtilsTest.php | 112 | ||||
-rw-r--r-- | tpl/default/changetag.html | 4 | ||||
-rw-r--r-- | tpl/default/css/shaarli.css | 54 | ||||
-rw-r--r-- | tpl/default/js/shaarli.js | 194 | ||||
-rw-r--r-- | tpl/default/page.footer.html | 3 | ||||
-rw-r--r-- | tpl/default/tag.cloud.html (renamed from tpl/default/tagcloud.html) | 4 | ||||
-rw-r--r-- | tpl/default/tag.list.html | 86 | ||||
-rw-r--r-- | tpl/default/tag.sort.html | 8 | ||||
-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 | */ | ||
449 | function 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 | } | ||
@@ -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 | */ | ||
530 | function 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 | */ | ||
556 | function 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 | */ | ||
574 | function 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 | */ | ||
599 | function htmlEntities(str) | ||
600 | { | ||
601 | return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) { | ||
602 | return '&#'+i.charCodeAt(0)+';'; | ||
603 | }); | ||
604 | } | ||
605 | |||
417 | function activateFirefoxSocial(node) { | 606 | function 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 | */ |
448 | function hideTimezoneCities(cities, currentContinent, reset = false) { | 637 | function 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> | ||
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> · | ||
5 | <a href="?do=taglist&sort=usage" title="cloud">{'Most used'|t}</a> · | ||
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 | |||