diff options
26 files changed, 847 insertions, 89 deletions
diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php index a1f4da48..7377bcec 100644 --- a/application/FeedBuilder.php +++ b/application/FeedBuilder.php | |||
@@ -97,6 +97,11 @@ class FeedBuilder | |||
97 | */ | 97 | */ |
98 | public function buildData() | 98 | public function buildData() |
99 | { | 99 | { |
100 | // Search for untagged links | ||
101 | if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { | ||
102 | $this->userInput['searchtags'] = false; | ||
103 | } | ||
104 | |||
100 | // Optionally filter the results: | 105 | // Optionally filter the results: |
101 | $linksToDisplay = $this->linkDB->filterSearch($this->userInput); | 106 | $linksToDisplay = $this->linkDB->filterSearch($this->userInput); |
102 | 107 | ||
diff --git a/application/LinkDB.php b/application/LinkDB.php index 0d3c85bd..8ca0fab3 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -423,43 +423,29 @@ You use the community supported version of the original Shaarli project, by Seba | |||
423 | public function filterSearch($filterRequest = array(), $casesensitive = false, $visibility = 'all') | 423 | public function filterSearch($filterRequest = array(), $casesensitive = false, $visibility = 'all') |
424 | { | 424 | { |
425 | // Filter link database according to parameters. | 425 | // Filter link database according to parameters. |
426 | $searchtags = !empty($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; | 426 | $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; |
427 | $searchterm = !empty($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; | 427 | $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; |
428 | 428 | ||
429 | // Search tags + fullsearch. | 429 | // Search tags + fullsearch - blank string parameter will return all links. |
430 | if (! empty($searchtags) && ! empty($searchterm)) { | 430 | $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; |
431 | $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; | 431 | $request = [$searchtags, $searchterm]; |
432 | $request = array($searchtags, $searchterm); | ||
433 | } | ||
434 | // Search by tags. | ||
435 | elseif (! empty($searchtags)) { | ||
436 | $type = LinkFilter::$FILTER_TAG; | ||
437 | $request = $searchtags; | ||
438 | } | ||
439 | // Fulltext search. | ||
440 | elseif (! empty($searchterm)) { | ||
441 | $type = LinkFilter::$FILTER_TEXT; | ||
442 | $request = $searchterm; | ||
443 | } | ||
444 | // Otherwise, display without filtering. | ||
445 | else { | ||
446 | $type = ''; | ||
447 | $request = ''; | ||
448 | } | ||
449 | 432 | ||
450 | $linkFilter = new LinkFilter($this); | 433 | $linkFilter = new LinkFilter($this); |
451 | return $linkFilter->filter($type, $request, $casesensitive, $visibility); | 434 | return $linkFilter->filter($type, $request, $casesensitive, $visibility); |
452 | } | 435 | } |
453 | 436 | ||
454 | /** | 437 | /** |
455 | * Returns the list of all tags | 438 | * Returns the list tags appearing in the links with the given tags |
456 | * Output: associative array key=tags, value=0 | 439 | * @param $filteringTags: tags selecting the links to consider |
440 | * @param $visibility: process only all/private/public links | ||
441 | * @return: a tag=>linksCount array | ||
457 | */ | 442 | */ |
458 | public function allTags() | 443 | public function linksCountPerTag($filteringTags = [], $visibility = 'all') |
459 | { | 444 | { |
445 | $links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility); | ||
460 | $tags = array(); | 446 | $tags = array(); |
461 | $caseMapping = array(); | 447 | $caseMapping = array(); |
462 | foreach ($this->links as $link) { | 448 | foreach ($links as $link) { |
463 | foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { | 449 | foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { |
464 | if (empty($tag)) { | 450 | if (empty($tag)) { |
465 | continue; | 451 | continue; |
diff --git a/application/LinkFilter.php b/application/LinkFilter.php index 81832a4b..0e887d38 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php | |||
@@ -253,6 +253,9 @@ class LinkFilter | |||
253 | { | 253 | { |
254 | // Implode if array for clean up. | 254 | // Implode if array for clean up. |
255 | $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags; | 255 | $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags; |
256 | if ($tags === false) { | ||
257 | return $this->filterUntagged($visibility); | ||
258 | } | ||
256 | if (empty($tags)) { | 259 | if (empty($tags)) { |
257 | return $this->noFilter($visibility); | 260 | return $this->noFilter($visibility); |
258 | } | 261 | } |
@@ -296,6 +299,33 @@ class LinkFilter | |||
296 | } | 299 | } |
297 | 300 | ||
298 | /** | 301 | /** |
302 | * Return only links without any tag. | ||
303 | * | ||
304 | * @param string $visibility return only all/private/public links. | ||
305 | * | ||
306 | * @return array filtered links. | ||
307 | */ | ||
308 | public function filterUntagged($visibility) | ||
309 | { | ||
310 | $filtered = []; | ||
311 | foreach ($this->links as $key => $link) { | ||
312 | if ($visibility !== 'all') { | ||
313 | if (! $link['private'] && $visibility === 'private') { | ||
314 | continue; | ||
315 | } else if ($link['private'] && $visibility === 'public') { | ||
316 | continue; | ||
317 | } | ||
318 | } | ||
319 | |||
320 | if (empty(trim($link['tags']))) { | ||
321 | $filtered[$key] = $link; | ||
322 | } | ||
323 | } | ||
324 | |||
325 | return $filtered; | ||
326 | } | ||
327 | |||
328 | /** | ||
299 | * Returns the list of articles for a given day, chronologically sorted | 329 | * Returns the list of articles for a given day, chronologically sorted |
300 | * | 330 | * |
301 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. | 331 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. |
diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 50e3f124..c86621a2 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php | |||
@@ -89,7 +89,7 @@ class PageBuilder | |||
89 | $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); | 89 | $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); |
90 | $this->tpl->assign('token', getToken($this->conf)); | 90 | $this->tpl->assign('token', getToken($this->conf)); |
91 | if ($this->linkDB !== null) { | 91 | if ($this->linkDB !== null) { |
92 | $this->tpl->assign('tags', $this->linkDB->allTags()); | 92 | $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); |
93 | } | 93 | } |
94 | // To be removed with a proper theme configuration. | 94 | // To be removed with a proper theme configuration. |
95 | $this->tpl->assign('conf', $this->conf); | 95 | $this->tpl->assign('conf', $this->conf); |
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..4a2f5561 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -91,6 +91,10 @@ function endsWith($haystack, $needle, $case = true) | |||
91 | */ | 91 | */ |
92 | function escape($input) | 92 | function escape($input) |
93 | { | 93 | { |
94 | if (is_bool($input)) { | ||
95 | return $input; | ||
96 | } | ||
97 | |||
94 | if (is_array($input)) { | 98 | if (is_array($input)) { |
95 | $out = array(); | 99 | $out = array(); |
96 | foreach($input as $key => $value) { | 100 | foreach($input as $key => $value) { |
@@ -435,3 +439,34 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true) | |||
435 | $maxsize = min($size1, $size2); | 439 | $maxsize = min($size1, $size2); |
436 | return $format ? human_bytes($maxsize) : $maxsize; | 440 | return $format ? human_bytes($maxsize) : $maxsize; |
437 | } | 441 | } |
442 | |||
443 | /** | ||
444 | * Sort the given array alphabetically using php-intl if available. | ||
445 | * Case sensitive. | ||
446 | * | ||
447 | * Note: doesn't support multidimensional arrays | ||
448 | * | ||
449 | * @param array $data Input array, passed by reference | ||
450 | * @param bool $reverse Reverse sort if set to true | ||
451 | * @param bool $byKeys Sort the array by keys if set to true, by value otherwise. | ||
452 | */ | ||
453 | function alphabetical_sort(&$data, $reverse = false, $byKeys = false) | ||
454 | { | ||
455 | $callback = function($a, $b) use ($reverse) { | ||
456 | // Collator is part of PHP intl. | ||
457 | if (class_exists('Collator')) { | ||
458 | $collator = new Collator(setlocale(LC_COLLATE, 0)); | ||
459 | if (!intl_is_failure(intl_get_error_code())) { | ||
460 | return $collator->compare($a, $b) * ($reverse ? -1 : 1); | ||
461 | } | ||
462 | } | ||
463 | |||
464 | return strcasecmp($a, $b) * ($reverse ? -1 : 1); | ||
465 | }; | ||
466 | |||
467 | if ($byKeys) { | ||
468 | uksort($data, $callback); | ||
469 | } else { | ||
470 | usort($data, $callback); | ||
471 | } | ||
472 | } | ||
@@ -790,7 +790,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
790 | // -------- Tag cloud | 790 | // -------- Tag cloud |
791 | if ($targetPage == Router::$PAGE_TAGCLOUD) | 791 | if ($targetPage == Router::$PAGE_TAGCLOUD) |
792 | { | 792 | { |
793 | $tags= $LINKSDB->allTags(); | 793 | $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; |
794 | $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; | ||
795 | $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); | ||
794 | 796 | ||
795 | // 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. |
796 | // First, find max value. | 798 | // First, find max value. |
@@ -799,17 +801,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
799 | $maxcount = max($maxcount, $value); | 801 | $maxcount = max($maxcount, $value); |
800 | } | 802 | } |
801 | 803 | ||
802 | // Sort tags alphabetically: case insensitive, support locale if available. | 804 | alphabetical_sort($tags, true, true); |
803 | uksort($tags, function($a, $b) { | ||
804 | // Collator is part of PHP intl. | ||
805 | if (class_exists('Collator')) { | ||
806 | $c = new Collator(setlocale(LC_COLLATE, 0)); | ||
807 | if (!intl_is_failure(intl_get_error_code())) { | ||
808 | return $c->compare($a, $b); | ||
809 | } | ||
810 | } | ||
811 | return strcasecmp($a, $b); | ||
812 | }); | ||
813 | 805 | ||
814 | $tagList = array(); | 806 | $tagList = array(); |
815 | foreach($tags as $key => $value) { | 807 | foreach($tags as $key => $value) { |
@@ -824,6 +816,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
824 | } | 816 | } |
825 | 817 | ||
826 | $data = array( | 818 | $data = array( |
819 | 'search_tags' => implode(' ', $filteringTags), | ||
827 | 'tags' => $tagList, | 820 | 'tags' => $tagList, |
828 | ); | 821 | ); |
829 | $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); | 822 | $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); |
@@ -832,7 +825,32 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
832 | $PAGE->assign($key, $value); | 825 | $PAGE->assign($key, $value); |
833 | } | 826 | } |
834 | 827 | ||
835 | $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'); | ||
836 | exit; | 854 | exit; |
837 | } | 855 | } |
838 | 856 | ||
@@ -1149,6 +1167,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1149 | if ($targetPage == Router::$PAGE_CHANGETAG) | 1167 | if ($targetPage == Router::$PAGE_CHANGETAG) |
1150 | { | 1168 | { |
1151 | 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']) : ''); | ||
1152 | $PAGE->renderPage('changetag'); | 1171 | $PAGE->renderPage('changetag'); |
1153 | exit; | 1172 | exit; |
1154 | } | 1173 | } |
@@ -1302,18 +1321,21 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1302 | // -------- User clicked the "Delete" button when editing a link: Delete link from database. | 1321 | // -------- User clicked the "Delete" button when editing a link: Delete link from database. |
1303 | if ($targetPage == Router::$PAGE_DELETELINK) | 1322 | if ($targetPage == Router::$PAGE_DELETELINK) |
1304 | { | 1323 | { |
1305 | // We do not need to ask for confirmation: | ||
1306 | // - confirmation is handled by JavaScript | ||
1307 | // - we are protected from XSRF by the token. | ||
1308 | |||
1309 | if (! tokenOk($_GET['token'])) { | 1324 | if (! tokenOk($_GET['token'])) { |
1310 | die('Wrong token.'); | 1325 | die('Wrong token.'); |
1311 | } | 1326 | } |
1312 | 1327 | ||
1313 | $id = intval(escape($_GET['lf_linkdate'])); | 1328 | if (strpos($_GET['lf_linkdate'], ' ') !== false) { |
1314 | $link = $LINKSDB[$id]; | 1329 | $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate'])))); |
1315 | $pluginManager->executeHooks('delete_link', $link); | 1330 | } else { |
1316 | unset($LINKSDB[$id]); | 1331 | $ids = [$_GET['lf_linkdate']]; |
1332 | } | ||
1333 | foreach ($ids as $id) { | ||
1334 | $id = (int) escape($id); | ||
1335 | $link = $LINKSDB[$id]; | ||
1336 | $pluginManager->executeHooks('delete_link', $link); | ||
1337 | unset($LINKSDB[$id]); | ||
1338 | } | ||
1317 | $LINKSDB->save($conf->get('resource.page_cache')); // save to disk | 1339 | $LINKSDB->save($conf->get('resource.page_cache')); // save to disk |
1318 | $history->deleteLink($link); | 1340 | $history->deleteLink($link); |
1319 | 1341 | ||
@@ -1345,7 +1367,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1345 | 'link' => $link, | 1367 | 'link' => $link, |
1346 | 'link_is_new' => false, | 1368 | 'link_is_new' => false, |
1347 | 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), | 1369 | 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), |
1348 | 'tags' => $LINKSDB->allTags(), | 1370 | 'tags' => $LINKSDB->linksCountPerTag(), |
1349 | ); | 1371 | ); |
1350 | $pluginManager->executeHooks('render_editlink', $data); | 1372 | $pluginManager->executeHooks('render_editlink', $data); |
1351 | 1373 | ||
@@ -1414,7 +1436,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1414 | 'link_is_new' => $link_is_new, | 1436 | 'link_is_new' => $link_is_new, |
1415 | 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), | 1437 | 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), |
1416 | 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), | 1438 | 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), |
1417 | 'tags' => $LINKSDB->allTags(), | 1439 | 'tags' => $LINKSDB->linksCountPerTag(), |
1418 | 'default_private_links' => $conf->get('privacy.default_private_links', false), | 1440 | 'default_private_links' => $conf->get('privacy.default_private_links', false), |
1419 | ); | 1441 | ); |
1420 | $pluginManager->executeHooks('render_editlink', $data); | 1442 | $pluginManager->executeHooks('render_editlink', $data); |
@@ -1570,6 +1592,13 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1570 | exit; | 1592 | exit; |
1571 | } | 1593 | } |
1572 | 1594 | ||
1595 | // Get a fresh token | ||
1596 | if ($targetPage == Router::$GET_TOKEN) { | ||
1597 | header('Content-Type:text/plain'); | ||
1598 | echo getToken($conf); | ||
1599 | exit; | ||
1600 | } | ||
1601 | |||
1573 | // -------- Otherwise, simply display search form and links: | 1602 | // -------- Otherwise, simply display search form and links: |
1574 | showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); | 1603 | showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); |
1575 | exit; | 1604 | exit; |
@@ -1587,7 +1616,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1587 | function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) | 1616 | function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) |
1588 | { | 1617 | { |
1589 | // Used in templates | 1618 | // Used in templates |
1590 | $searchtags = !empty($_GET['searchtags']) ? escape(normalize_spaces($_GET['searchtags'])) : ''; | 1619 | if (isset($_GET['searchtags'])) { |
1620 | if (! empty($_GET['searchtags'])) { | ||
1621 | $searchtags = escape(normalize_spaces($_GET['searchtags'])); | ||
1622 | } else { | ||
1623 | $searchtags = false; | ||
1624 | } | ||
1625 | } else { | ||
1626 | $searchtags = ''; | ||
1627 | } | ||
1591 | $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : ''; | 1628 | $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : ''; |
1592 | 1629 | ||
1593 | // Smallhash filter | 1630 | // Smallhash filter |
@@ -1602,7 +1639,11 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) | |||
1602 | } else { | 1639 | } else { |
1603 | // Filter links according search parameters. | 1640 | // Filter links according search parameters. |
1604 | $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; | 1641 | $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; |
1605 | $linksToDisplay = $LINKSDB->filterSearch($_GET, false, $visibility); | 1642 | $request = [ |
1643 | 'searchtags' => $searchtags, | ||
1644 | 'searchterm' => $searchterm, | ||
1645 | ]; | ||
1646 | $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility); | ||
1606 | } | 1647 | } |
1607 | 1648 | ||
1608 | // ---- Handle paging. | 1649 | // ---- Handle paging. |
@@ -1649,7 +1690,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) | |||
1649 | } | 1690 | } |
1650 | 1691 | ||
1651 | // Compute paging navigation | 1692 | // Compute paging navigation |
1652 | $searchtagsUrl = empty($searchtags) ? '' : '&searchtags=' . urlencode($searchtags); | 1693 | $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); |
1653 | $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); | 1694 | $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); |
1654 | $previous_page_url = ''; | 1695 | $previous_page_url = ''; |
1655 | if ($i != count($keys)) { | 1696 | if ($i != count($keys)) { |
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 7bf98f92..25438277 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php | |||
@@ -297,7 +297,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
297 | 'sTuff' => 2, | 297 | 'sTuff' => 2, |
298 | 'ut' => 1, | 298 | 'ut' => 1, |
299 | ), | 299 | ), |
300 | self::$publicLinkDB->allTags() | 300 | self::$publicLinkDB->linksCountPerTag() |
301 | ); | 301 | ); |
302 | 302 | ||
303 | $this->assertEquals( | 303 | $this->assertEquals( |
@@ -325,7 +325,34 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
325 | 'tag4' => 1, | 325 | 'tag4' => 1, |
326 | 'ut' => 1, | 326 | 'ut' => 1, |
327 | ), | 327 | ), |
328 | self::$privateLinkDB->allTags() | 328 | self::$privateLinkDB->linksCountPerTag() |
329 | ); | ||
330 | $this->assertEquals( | ||
331 | array( | ||
332 | 'web' => 4, | ||
333 | 'cartoon' => 2, | ||
334 | 'gnu' => 1, | ||
335 | 'dev' => 1, | ||
336 | 'samba' => 1, | ||
337 | 'media' => 1, | ||
338 | 'html' => 1, | ||
339 | 'w3c' => 1, | ||
340 | 'css' => 1, | ||
341 | 'Mercurial' => 1, | ||
342 | '.hidden' => 1, | ||
343 | 'hashtag' => 1, | ||
344 | ), | ||
345 | self::$privateLinkDB->linksCountPerTag(['web']) | ||
346 | ); | ||
347 | $this->assertEquals( | ||
348 | array( | ||
349 | 'web' => 1, | ||
350 | 'html' => 1, | ||
351 | 'w3c' => 1, | ||
352 | 'css' => 1, | ||
353 | 'Mercurial' => 1, | ||
354 | ), | ||
355 | self::$privateLinkDB->linksCountPerTag(['web'], 'private') | ||
329 | ); | 356 | ); |
330 | } | 357 | } |
331 | 358 | ||
@@ -448,7 +475,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
448 | public function testReorderLinksDesc() | 475 | public function testReorderLinksDesc() |
449 | { | 476 | { |
450 | self::$privateLinkDB->reorder('ASC'); | 477 | self::$privateLinkDB->reorder('ASC'); |
451 | $linkIds = array(42, 4, 1, 0, 7, 6, 8, 41); | 478 | $linkIds = array(42, 4, 9, 1, 0, 7, 6, 8, 41); |
452 | $cpt = 0; | 479 | $cpt = 0; |
453 | foreach (self::$privateLinkDB as $key => $value) { | 480 | foreach (self::$privateLinkDB as $key => $value) { |
454 | $this->assertEquals($linkIds[$cpt++], $key); | 481 | $this->assertEquals($linkIds[$cpt++], $key); |
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php index 37d5ca30..74162358 100644 --- a/tests/LinkFilterTest.php +++ b/tests/LinkFilterTest.php | |||
@@ -63,6 +63,12 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase | |||
63 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '')) | 63 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '')) |
64 | ); | 64 | ); |
65 | 65 | ||
66 | // Untagged only | ||
67 | $this->assertEquals( | ||
68 | self::$refDB->countUntaggedLinks(), | ||
69 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, false)) | ||
70 | ); | ||
71 | |||
66 | $this->assertEquals( | 72 | $this->assertEquals( |
67 | ReferenceLinkDB::$NB_LINKS_TOTAL, | 73 | ReferenceLinkDB::$NB_LINKS_TOTAL, |
68 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '')) | 74 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '')) |
@@ -146,7 +152,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase | |||
146 | public function testFilterDay() | 152 | public function testFilterDay() |
147 | { | 153 | { |
148 | $this->assertEquals( | 154 | $this->assertEquals( |
149 | 3, | 155 | 4, |
150 | count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206')) | 156 | count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206')) |
151 | ); | 157 | ); |
152 | } | 158 | } |
@@ -339,7 +345,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase | |||
339 | ); | 345 | ); |
340 | 346 | ||
341 | $this->assertEquals( | 347 | $this->assertEquals( |
342 | 7, | 348 | ReferenceLinkDB::$NB_LINKS_TOTAL - 1, |
343 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution')) | 349 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution')) |
344 | ); | 350 | ); |
345 | } | 351 | } |
@@ -399,7 +405,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase | |||
399 | ); | 405 | ); |
400 | 406 | ||
401 | $this->assertEquals( | 407 | $this->assertEquals( |
402 | 7, | 408 | ReferenceLinkDB::$NB_LINKS_TOTAL - 1, |
403 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) | 409 | count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) |
404 | ); | 410 | ); |
405 | } | 411 | } |
@@ -429,6 +435,13 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase | |||
429 | 1, | 435 | 1, |
430 | count(self::$linkFilter->filter( | 436 | count(self::$linkFilter->filter( |
431 | LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, | 437 | LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, |
438 | array(false, 'PSR-2') | ||
439 | )) | ||
440 | ); | ||
441 | $this->assertEquals( | ||
442 | 1, | ||
443 | count(self::$linkFilter->filter( | ||
444 | LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, | ||
432 | array($tags, '') | 445 | array($tags, '') |
433 | )) | 446 | )) |
434 | ); | 447 | ); |
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/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/GetLinksTest.php index 84ae7f7a..4cb70224 100644 --- a/tests/api/controllers/GetLinksTest.php +++ b/tests/api/controllers/GetLinksTest.php | |||
@@ -95,7 +95,7 @@ class GetLinksTest extends \PHPUnit_Framework_TestCase | |||
95 | $this->assertEquals($this->refDB->countLinks(), count($data)); | 95 | $this->assertEquals($this->refDB->countLinks(), count($data)); |
96 | 96 | ||
97 | // Check order | 97 | // Check order |
98 | $order = [41, 8, 6, 7, 0, 1, 4, 42]; | 98 | $order = [41, 8, 6, 7, 0, 1, 9, 4, 42]; |
99 | $cpt = 0; | 99 | $cpt = 0; |
100 | foreach ($data as $link) { | 100 | foreach ($data as $link) { |
101 | $this->assertEquals(self::NB_FIELDS_LINK, count($link)); | 101 | $this->assertEquals(self::NB_FIELDS_LINK, count($link)); |
@@ -164,7 +164,7 @@ class GetLinksTest extends \PHPUnit_Framework_TestCase | |||
164 | $data = json_decode((string) $response->getBody(), true); | 164 | $data = json_decode((string) $response->getBody(), true); |
165 | $this->assertEquals($this->refDB->countLinks(), count($data)); | 165 | $this->assertEquals($this->refDB->countLinks(), count($data)); |
166 | // Check order | 166 | // Check order |
167 | $order = [41, 8, 6, 7, 0, 1, 4, 42]; | 167 | $order = [41, 8, 6, 7, 0, 1, 9, 4, 42]; |
168 | $cpt = 0; | 168 | $cpt = 0; |
169 | foreach ($data as $link) { | 169 | foreach ($data as $link) { |
170 | $this->assertEquals(self::NB_FIELDS_LINK, count($link)); | 170 | $this->assertEquals(self::NB_FIELDS_LINK, count($link)); |
diff --git a/tests/api/controllers/InfoTest.php b/tests/api/controllers/InfoTest.php index e85eb281..f7e63bfa 100644 --- a/tests/api/controllers/InfoTest.php +++ b/tests/api/controllers/InfoTest.php | |||
@@ -81,7 +81,7 @@ class InfoTest extends \PHPUnit_Framework_TestCase | |||
81 | $this->assertEquals(200, $response->getStatusCode()); | 81 | $this->assertEquals(200, $response->getStatusCode()); |
82 | $data = json_decode((string) $response->getBody(), true); | 82 | $data = json_decode((string) $response->getBody(), true); |
83 | 83 | ||
84 | $this->assertEquals(8, $data['global_counter']); | 84 | $this->assertEquals(\ReferenceLinkDB::$NB_LINKS_TOTAL, $data['global_counter']); |
85 | $this->assertEquals(2, $data['private_counter']); | 85 | $this->assertEquals(2, $data['private_counter']); |
86 | $this->assertEquals('Shaarli', $data['settings']['title']); | 86 | $this->assertEquals('Shaarli', $data['settings']['title']); |
87 | $this->assertEquals('?', $data['settings']['header_link']); | 87 | $this->assertEquals('?', $data['settings']['header_link']); |
@@ -104,7 +104,7 @@ class InfoTest extends \PHPUnit_Framework_TestCase | |||
104 | $this->assertEquals(200, $response->getStatusCode()); | 104 | $this->assertEquals(200, $response->getStatusCode()); |
105 | $data = json_decode((string) $response->getBody(), true); | 105 | $data = json_decode((string) $response->getBody(), true); |
106 | 106 | ||
107 | $this->assertEquals(8, $data['global_counter']); | 107 | $this->assertEquals(\ReferenceLinkDB::$NB_LINKS_TOTAL, $data['global_counter']); |
108 | $this->assertEquals(2, $data['private_counter']); | 108 | $this->assertEquals(2, $data['private_counter']); |
109 | $this->assertEquals($title, $data['settings']['title']); | 109 | $this->assertEquals($title, $data['settings']['title']); |
110 | $this->assertEquals($headerLink, $data['settings']['header_link']); | 110 | $this->assertEquals($headerLink, $data['settings']['header_link']); |
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index 1f4b3063..f09eebc1 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php | |||
@@ -4,7 +4,7 @@ | |||
4 | */ | 4 | */ |
5 | class ReferenceLinkDB | 5 | class ReferenceLinkDB |
6 | { | 6 | { |
7 | public static $NB_LINKS_TOTAL = 8; | 7 | public static $NB_LINKS_TOTAL = 9; |
8 | 8 | ||
9 | private $_links = array(); | 9 | private $_links = array(); |
10 | private $_publicCount = 0; | 10 | private $_publicCount = 0; |
@@ -38,6 +38,16 @@ class ReferenceLinkDB | |||
38 | ); | 38 | ); |
39 | 39 | ||
40 | $this->addLink( | 40 | $this->addLink( |
41 | 9, | ||
42 | 'PSR-2: Coding Style Guide', | ||
43 | 'http://www.php-fig.org/psr/psr-2/', | ||
44 | 'This guide extends and expands on PSR-1, the basic coding standard.', | ||
45 | 0, | ||
46 | DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_152312'), | ||
47 | '' | ||
48 | ); | ||
49 | |||
50 | $this->addLink( | ||
41 | 8, | 51 | 8, |
42 | 'Free as in Freedom 2.0 @website', | 52 | 'Free as in Freedom 2.0 @website', |
43 | 'https://static.fsf.org/nosvn/faif-2.0.pdf', | 53 | 'https://static.fsf.org/nosvn/faif-2.0.pdf', |
@@ -161,6 +171,20 @@ class ReferenceLinkDB | |||
161 | return $this->_privateCount; | 171 | return $this->_privateCount; |
162 | } | 172 | } |
163 | 173 | ||
174 | /** | ||
175 | * Returns the number of links without tag | ||
176 | */ | ||
177 | public function countUntaggedLinks() | ||
178 | { | ||
179 | $cpt = 0; | ||
180 | foreach ($this->_links as $link) { | ||
181 | if (empty($link['tags'])) { | ||
182 | ++$cpt; | ||
183 | } | ||
184 | } | ||
185 | return $cpt; | ||
186 | } | ||
187 | |||
164 | public function getLinks() | 188 | public function getLinks() |
165 | { | 189 | { |
166 | return $this->_links; | 190 | return $this->_links; |
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 73fade5f..3391fa05 100644 --- a/tpl/default/css/shaarli.css +++ b/tpl/default/css/shaarli.css | |||
@@ -211,7 +211,7 @@ body, .pure-g [class*="pure-u"] { | |||
211 | } | 211 | } |
212 | } | 212 | } |
213 | 213 | ||
214 | #search, #search-linklist { | 214 | #search, #search-linklist, #search-tagcloud { |
215 | text-align: center; | 215 | text-align: center; |
216 | width: 100%; | 216 | width: 100%; |
217 | } | 217 | } |
@@ -234,6 +234,7 @@ body, .pure-g [class*="pure-u"] { | |||
234 | } | 234 | } |
235 | 235 | ||
236 | #search button, | 236 | #search button, |
237 | #search-tagcloud button, | ||
237 | #search-linklist button { | 238 | #search-linklist button { |
238 | background: transparent; | 239 | background: transparent; |
239 | border: none; | 240 | border: none; |
@@ -251,6 +252,9 @@ body, .pure-g [class*="pure-u"] { | |||
251 | #search-linklist button:hover { | 252 | #search-linklist button:hover { |
252 | color: #fff; | 253 | color: #fff; |
253 | } | 254 | } |
255 | #search-tagcloud button:hover { | ||
256 | color: #d0d0d0; | ||
257 | } | ||
254 | 258 | ||
255 | #search-linklist { | 259 | #search-linklist { |
256 | padding: 5px 0; | 260 | padding: 5px 0; |
@@ -275,6 +279,19 @@ body, .pure-g [class*="pure-u"] { | |||
275 | } | 279 | } |
276 | } | 280 | } |
277 | 281 | ||
282 | .subheader-form a.button { | ||
283 | color: #f5f5f5; | ||
284 | font-weight: bold; | ||
285 | text-decoration: none; | ||
286 | border: 2px solid #f5f5f5; | ||
287 | border-radius: 5px; | ||
288 | padding: 3px 10px; | ||
289 | } | ||
290 | |||
291 | .linklist-item-editbuttons .delete-checkbox { | ||
292 | display: none; | ||
293 | } | ||
294 | |||
278 | #header-login-form input[type="text"], #header-login-form input[type="password"] { | 295 | #header-login-form input[type="text"], #header-login-form input[type="password"] { |
279 | width: 200px; | 296 | width: 200px; |
280 | } | 297 | } |
@@ -522,8 +539,8 @@ body, .pure-g [class*="pure-u"] { | |||
522 | color: #1b926c; | 539 | color: #1b926c; |
523 | } | 540 | } |
524 | 541 | ||
525 | .linklist-item-title .linklist-link:visited { | 542 | .linklist-item-title a:visited .linklist-link { |
526 | color: #1b926c; | 543 | color: #555555; |
527 | } | 544 | } |
528 | 545 | ||
529 | .linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ | 546 | .linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ |
@@ -734,10 +751,11 @@ body, .pure-g [class*="pure-u"] { | |||
734 | .page-form a { | 751 | .page-form a { |
735 | color: #1b926c; | 752 | color: #1b926c; |
736 | font-weight: bold; | 753 | font-weight: bold; |
754 | text-decoration: none; | ||
737 | } | 755 | } |
738 | 756 | ||
739 | .page-form p { | 757 | .page-form p { |
740 | padding: 0 10px; | 758 | padding: 5px 10px; |
741 | margin: 0; | 759 | margin: 0; |
742 | } | 760 | } |
743 | 761 | ||
@@ -1053,7 +1071,7 @@ form[name="linkform"].page-form { | |||
1053 | } | 1071 | } |
1054 | 1072 | ||
1055 | #cloudtag, #cloudtag a { | 1073 | #cloudtag, #cloudtag a { |
1056 | color: #000; | 1074 | color: #252525; |
1057 | text-decoration: none; | 1075 | text-decoration: none; |
1058 | } | 1076 | } |
1059 | 1077 | ||
@@ -1062,6 +1080,42 @@ form[name="linkform"].page-form { | |||
1062 | } | 1080 | } |
1063 | 1081 | ||
1064 | /** | 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 | /** | ||
1065 | * Picture wall CSS | 1119 | * Picture wall CSS |
1066 | */ | 1120 | */ |
1067 | #picwall_container { | 1121 | #picwall_container { |
@@ -1210,3 +1264,16 @@ form[name="linkform"].page-form { | |||
1210 | .pure-button { | 1264 | .pure-button { |
1211 | -moz-user-select: auto; | 1265 | -moz-user-select: auto; |
1212 | } | 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 4d47fcd0..4ebb7815 100644 --- a/tpl/default/js/shaarli.js +++ b/tpl/default/js/shaarli.js | |||
@@ -216,14 +216,14 @@ window.onload = function () { | |||
216 | /** | 216 | /** |
217 | * Autofocus text fields | 217 | * Autofocus text fields |
218 | */ | 218 | */ |
219 | // ES6 syntax | 219 | var autofocusElements = document.querySelectorAll('.autofocus'); |
220 | let autofocusElements = document.querySelectorAll('.autofocus'); | 220 | var breakLoop = false; |
221 | for (let autofocusElement of autofocusElements) { | 221 | [].forEach.call(autofocusElements, function(autofocusElement) { |
222 | if (autofocusElement.value == '') { | 222 | if (autofocusElement.value == '' && ! breakLoop) { |
223 | autofocusElement.focus(); | 223 | autofocusElement.focus(); |
224 | break; | 224 | breakLoop = true; |
225 | } | 225 | } |
226 | } | 226 | }); |
227 | 227 | ||
228 | /** | 228 | /** |
229 | * Handle sub menus/forms | 229 | * Handle sub menus/forms |
@@ -357,13 +357,252 @@ window.onload = function () { | |||
357 | var continent = document.getElementById('continent'); | 357 | var continent = document.getElementById('continent'); |
358 | var city = document.getElementById('city'); | 358 | var city = document.getElementById('city'); |
359 | if (continent != null && city != null) { | 359 | if (continent != null && city != null) { |
360 | continent.addEventListener('change', function(event) { | 360 | continent.addEventListener('change', function (event) { |
361 | hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true); | 361 | hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true); |
362 | }); | 362 | }); |
363 | hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false); | 363 | hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false); |
364 | } | 364 | } |
365 | |||
366 | /** | ||
367 | * Bulk actions | ||
368 | */ | ||
369 | var linkCheckboxes = document.querySelectorAll('.delete-checkbox'); | ||
370 | var bar = document.getElementById('actions'); | ||
371 | [].forEach.call(linkCheckboxes, function(checkbox) { | ||
372 | checkbox.style.display = 'block'; | ||
373 | checkbox.addEventListener('click', function(event) { | ||
374 | var count = 0; | ||
375 | var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked'); | ||
376 | [].forEach.call(linkCheckedCheckboxes, function(checkbox) { | ||
377 | count++; | ||
378 | }); | ||
379 | if (count == 0 && bar.classList.contains('open')) { | ||
380 | bar.classList.toggle('open'); | ||
381 | } else if (count > 0 && ! bar.classList.contains('open')) { | ||
382 | bar.classList.toggle('open'); | ||
383 | } | ||
384 | }); | ||
385 | }); | ||
386 | |||
387 | var deleteButton = document.getElementById('actions-delete'); | ||
388 | var token = document.querySelector('input[type="hidden"][name="token"]'); | ||
389 | if (deleteButton != null && token != null) { | ||
390 | deleteButton.addEventListener('click', function(event) { | ||
391 | event.preventDefault(); | ||
392 | |||
393 | var links = []; | ||
394 | var linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked'); | ||
395 | [].forEach.call(linkCheckedCheckboxes, function(checkbox) { | ||
396 | links.push({ | ||
397 | 'id': checkbox.value, | ||
398 | 'title': document.querySelector('.linklist-item[data-id="'+ checkbox.value +'"] .linklist-link').innerHTML | ||
399 | }); | ||
400 | }); | ||
401 | |||
402 | var message = 'Are you sure you want to delete '+ links.length +' links?\n'; | ||
403 | message += 'This action is IRREVERSIBLE!\n\nTitles:\n'; | ||
404 | var ids = ''; | ||
405 | links.forEach(function(item) { | ||
406 | message += ' - '+ item['title'] +'\n'; | ||
407 | ids += item['id'] +'+'; | ||
408 | }); | ||
409 | |||
410 | if (window.confirm(message)) { | ||
411 | window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value; | ||
412 | } | ||
413 | }); | ||
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); | ||
365 | }; | 519 | }; |
366 | 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 | |||
367 | function activateFirefoxSocial(node) { | 606 | function activateFirefoxSocial(node) { |
368 | var loc = location.href; | 607 | var loc = location.href; |
369 | var baseURL = loc.substring(0, loc.lastIndexOf("/")); | 608 | var baseURL = loc.substring(0, loc.lastIndexOf("/")); |
@@ -395,9 +634,12 @@ function activateFirefoxSocial(node) { | |||
395 | * @param currentContinent Current selected continent | 634 | * @param currentContinent Current selected continent |
396 | * @param reset Set to true to reset the selected value | 635 | * @param reset Set to true to reset the selected value |
397 | */ | 636 | */ |
398 | function hideTimezoneCities(cities, currentContinent, reset = false) { | 637 | function hideTimezoneCities(cities, currentContinent) { |
399 | var first = true; | 638 | var first = true; |
400 | [].forEach.call(cities, function(option) { | 639 | if (reset == null) { |
640 | reset = false; | ||
641 | } | ||
642 | [].forEach.call(cities, function (option) { | ||
401 | if (option.getAttribute('data-continent') != currentContinent) { | 643 | if (option.getAttribute('data-continent') != currentContinent) { |
402 | option.className = 'hidden'; | 644 | option.className = 'hidden'; |
403 | } else { | 645 | } else { |
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index 57ef4567..2568a5d6 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html | |||
@@ -15,6 +15,8 @@ | |||
15 | {/if} | 15 | {/if} |
16 | </div> | 16 | </div> |
17 | 17 | ||
18 | <input type="hidden" name="token" value="{$token}"> | ||
19 | |||
18 | <div id="search-linklist"> | 20 | <div id="search-linklist"> |
19 | 21 | ||
20 | <div class="pure-g"> | 22 | <div class="pure-g"> |
@@ -89,7 +91,7 @@ | |||
89 | <div id="searchcriteria">{'Nothing found.'|t}</div> | 91 | <div id="searchcriteria">{'Nothing found.'|t}</div> |
90 | </div> | 92 | </div> |
91 | </div> | 93 | </div> |
92 | {elseif="!empty($search_term) or !empty($search_tags) or !empty($visibility)"} | 94 | {elseif="!empty($search_term) or $search_tags !== '' or !empty($visibility)"} |
93 | <div class="pure-g pure-alert pure-alert-success search-result"> | 95 | <div class="pure-g pure-alert pure-alert-success search-result"> |
94 | <div class="pure-u-2-24"></div> | 96 | <div class="pure-u-2-24"></div> |
95 | <div class="pure-u-20-24"> | 97 | <div class="pure-u-20-24"> |
@@ -105,6 +107,10 @@ | |||
105 | <a href="?removetag={function="urlencode($value)"}">{$value}<span class="remove"><i class="fa fa-times"></i></span></a> | 107 | <a href="?removetag={function="urlencode($value)"}">{$value}<span class="remove"><i class="fa fa-times"></i></span></a> |
106 | </span> | 108 | </span> |
107 | {/loop} | 109 | {/loop} |
110 | {elseif="$search_tags === false"} | ||
111 | <span class="label label-tag" title="{'Remove tag'|t}"> | ||
112 | <a href="?">{'untagged'|t}<span class="remove"><i class="fa fa-times"></i></span></a> | ||
113 | </span> | ||
108 | {/if} | 114 | {/if} |
109 | {if="!empty($visibility)"} | 115 | {if="!empty($visibility)"} |
110 | {'with status'|t} | 116 | {'with status'|t} |
@@ -121,7 +127,7 @@ | |||
121 | <div class="pure-u-lg-20-24 pure-u-22-24"> | 127 | <div class="pure-u-lg-20-24 pure-u-22-24"> |
122 | {loop="links"} | 128 | {loop="links"} |
123 | <div class="anchor" id="{$value.shorturl}"></div> | 129 | <div class="anchor" id="{$value.shorturl}"></div> |
124 | <div class="linklist-item{if="$value.class"} {$value.class}{/if}"> | 130 | <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> |
125 | 131 | ||
126 | <div class="linklist-item-title"> | 132 | <div class="linklist-item-title"> |
127 | {if="isLoggedIn()"} | 133 | {if="isLoggedIn()"} |
@@ -129,6 +135,7 @@ | |||
129 | {if="$value.private"} | 135 | {if="$value.private"} |
130 | <span class="label label-private">{'Private'|t}</span> | 136 | <span class="label label-private">{'Private'|t}</span> |
131 | {/if} | 137 | {/if} |
138 | <input type="checkbox" class="delete-checkbox" value="{$value.id}"> | ||
132 | <!-- FIXME! JS translation --> | 139 | <!-- FIXME! JS translation --> |
133 | <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a> | 140 | <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a> |
134 | <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a> | 141 | <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a> |
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/page.header.html b/tpl/default/page.header.html index 9388ef79..6c71a718 100644 --- a/tpl/default/page.header.html +++ b/tpl/default/page.header.html | |||
@@ -122,6 +122,13 @@ | |||
122 | </div> | 122 | </div> |
123 | </div> | 123 | </div> |
124 | </div> | 124 | </div> |
125 | <div id="actions" class="subheader-form"> | ||
126 | <div class="pure-g"> | ||
127 | <div class="pure-u-1"> | ||
128 | <a href="" id="actions-delete" class="button">Delete</a> | ||
129 | </div> | ||
130 | </div> | ||
131 | </div> | ||
125 | {if="!isLoggedIn()"} | 132 | {if="!isLoggedIn()"} |
126 | <form method="post" name="loginform"> | 133 | <form method="post" name="loginform"> |
127 | <div class="subheader-form" id="header-login-form"> | 134 | <div class="subheader-form" id="header-login-form"> |
diff --git a/tpl/default/tagcloud.html b/tpl/default/tag.cloud.html index 53c31748..59aa2ee0 100644 --- a/tpl/default/tagcloud.html +++ b/tpl/default/tag.cloud.html | |||
@@ -6,12 +6,32 @@ | |||
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"> |
12 | {$countTags=count($tags)} | 14 | {$countTags=count($tags)} |
13 | <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2> | 15 | <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2> |
14 | 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="tagcloud"> | ||
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 | |||
15 | <div id="plugin_zone_start_tagcloud" class="plugin_zone"> | 35 | <div id="plugin_zone_start_tagcloud" class="plugin_zone"> |
16 | {loop="$plugin_start_zone"} | 36 | {loop="$plugin_start_zone"} |
17 | {$value} | 37 | {$value} |
@@ -21,7 +41,7 @@ | |||
21 | <div id="cloudtag"> | 41 | <div id="cloudtag"> |
22 | {loop="tags"} | 42 | {loop="tags"} |
23 | <a href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a | 43 | <a href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a |
24 | ><span class="count">{$value.count}</span> | 44 | ><a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value.count}</a> |
25 | {loop="$value.tag_plugin"} | 45 | {loop="$value.tag_plugin"} |
26 | {$value} | 46 | {$value} |
27 | {/loop} | 47 | {/loop} |
@@ -36,6 +56,8 @@ | |||
36 | </div> | 56 | </div> |
37 | </div> | 57 | </div> |
38 | 58 | ||
59 | {include="tag.sort"} | ||
60 | |||
39 | {include="page.footer"} | 61 | {include="page.footer"} |
40 | </body> | 62 | </body> |
41 | </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/default/tools.html b/tpl/default/tools.html index baa033af..35173d17 100644 --- a/tpl/default/tools.html +++ b/tpl/default/tools.html | |||
@@ -72,10 +72,15 @@ | |||
72 | function(){ | 72 | function(){ |
73 | var%20url%20=%20location.href; | 73 | var%20url%20=%20location.href; |
74 | var%20title%20=%20document.title%20||%20url; | 74 | var%20title%20=%20document.title%20||%20url; |
75 | var%20desc=document.getSelection().toString(); | ||
76 | if(desc.length>4000){ | ||
77 | desc=desc.substr(0,4000)+'...'; | ||
78 | alert('{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}'); | ||
79 | } | ||
75 | window.open( | 80 | window.open( |
76 | '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+ | 81 | '{$pageabsaddr}?post='%20+%20encodeURIComponent(url)+ |
77 | '&title='%20+%20encodeURIComponent(title)+ | 82 | '&title='%20+%20encodeURIComponent(title)+ |
78 | '&description='%20+%20encodeURIComponent(document.getSelection())+ | 83 | '&description='%20+%20encodeURIComponent(desc)+ |
79 | '&source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' | 84 | '&source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' |
80 | ); | 85 | ); |
81 | } | 86 | } |
@@ -86,8 +91,21 @@ | |||
86 | <div class="tools-item"> | 91 | <div class="tools-item"> |
87 | <a title="{'Drag this link to your bookmarks toolbar or right-click it and Bookmark This Link'|t}, | 92 | <a title="{'Drag this link to your bookmarks toolbar or right-click it and Bookmark This Link'|t}, |
88 | {'Then click ✚Add Note button anytime to start composing a private Note (text post) to your Shaarli'|t}" | 93 | {'Then click ✚Add Note button anytime to start composing a private Note (text post) to your Shaarli'|t}" |
89 | href="?private=1&post=" | 94 | class="bookmarklet-link" |
90 | class="bookmarklet-link"> | 95 | href="javascript:( |
96 | function(){ | ||
97 | var%20desc=document.getSelection().toString(); | ||
98 | if(desc.length>4000){ | ||
99 | desc=desc.substr(0,4000)+'...'; | ||
100 | alert("{function="str_replace(' ', '%20', t('The selected text is too long, it will be truncated.'))"}"); | ||
101 | } | ||
102 | window.open( | ||
103 | '{$pageabsaddr}?private=1&post='+ | ||
104 | '&description='%20+%20encodeURIComponent(desc)+ | ||
105 | '&source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' | ||
106 | ); | ||
107 | } | ||
108 | )();"> | ||
91 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">✚ {'Add Note'|t}</span> | 109 | <span class="pure-button pure-u-lg-2-3 pure-u-3-4">✚ {'Add Note'|t}</span> |
92 | </a> | 110 | </a> |
93 | </div> | 111 | </div> |
@@ -146,4 +164,3 @@ | |||
146 | value="{'Drag this link to your bookmarks toolbar, or right-click it and choose Bookmark This Link'|t}"> | 164 | value="{'Drag this link to your bookmarks toolbar, or right-click it and choose Bookmark This Link'|t}"> |
147 | </body> | 165 | </body> |
148 | </html> | 166 | </html> |
149 | |||
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html index fc116667..8458caa1 100644 --- a/tpl/vintage/linklist.html +++ b/tpl/vintage/linklist.html | |||
@@ -55,7 +55,7 @@ | |||
55 | 55 | ||
56 | {if="count($links)==0"} | 56 | {if="count($links)==0"} |
57 | <div id="searchcriteria">Nothing found.</div> | 57 | <div id="searchcriteria">Nothing found.</div> |
58 | {elseif="!empty($search_term) or !empty($search_tags)"} | 58 | {elseif="!empty($search_term) or $search_tags !== ''"} |
59 | <div id="searchcriteria"> | 59 | <div id="searchcriteria"> |
60 | {$result_count} results | 60 | {$result_count} results |
61 | {if="!empty($search_term)"} | 61 | {if="!empty($search_term)"} |
@@ -69,6 +69,10 @@ | |||
69 | <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a> | 69 | <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a> |
70 | </span> | 70 | </span> |
71 | {/loop} | 71 | {/loop} |
72 | {elseif="$search_tags === false"} | ||
73 | <span class="linktag" title="Remove tag"> | ||
74 | <a href="?">untagged <span class="remove">x</span></a> | ||
75 | </span> | ||
72 | {/if} | 76 | {/if} |
73 | </div> | 77 | </div> |
74 | {/if} | 78 | {/if} |
diff --git a/tpl/vintage/tagcloud.html b/tpl/vintage/tag.cloud.html index 05e45273..d93bf4f9 100644 --- a/tpl/vintage/tagcloud.html +++ b/tpl/vintage/tag.cloud.html | |||
@@ -12,7 +12,7 @@ | |||
12 | 12 | ||
13 | <div id="cloudtag"> | 13 | <div id="cloudtag"> |
14 | {loop="$tags"} | 14 | {loop="$tags"} |
15 | <span class="count">{$value.count}</span><a | 15 | <a href="?addtag={$key|urlencode}" class="count">{$value.count}</a><a |
16 | href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a> | 16 | href="?searchtags={$key|urlencode}" style="font-size:{$value.size}em;">{$key}</a> |
17 | {loop="$value.tag_plugin"} | 17 | {loop="$value.tag_plugin"} |
18 | {$value} | 18 | {$value} |
diff --git a/tpl/vintage/tools.html b/tpl/vintage/tools.html index c36aa5b5..69689807 100644 --- a/tpl/vintage/tools.html +++ b/tpl/vintage/tools.html | |||
@@ -39,7 +39,15 @@ | |||
39 | </a><br><br> | 39 | </a><br><br> |
40 | <a class="smallbutton" | 40 | <a class="smallbutton" |
41 | onclick="return alertBookmarklet();" | 41 | onclick="return alertBookmarklet();" |
42 | href="?private=1&post="><b>✚Add Note</b></a> | 42 | href="javascript:( |
43 | function(){ | ||
44 | window.open( | ||
45 | '{$pageabsaddr}?private=1&post='+ | ||
46 | '&description='%20+%20encodeURIComponent(document.getSelection())+ | ||
47 | '&source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' | ||
48 | ); | ||
49 | } | ||
50 | )();"><b>✚Add Note</b></a> | ||
43 | <a href="#" onclick="return alertBookmarklet();"> | 51 | <a href="#" onclick="return alertBookmarklet();"> |
44 | <span> | 52 | <span> |
45 | ⇐ Drag this link to your bookmarks toolbar (or right-click it and choose Bookmark This Link....).<br> | 53 | ⇐ Drag this link to your bookmarks toolbar (or right-click it and choose Bookmark This Link....).<br> |