aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/FeedBuilder.php5
-rw-r--r--application/LinkDB.php38
-rw-r--r--application/LinkFilter.php30
-rw-r--r--application/PageBuilder.php2
-rw-r--r--application/Router.php12
-rw-r--r--application/Utils.php35
-rw-r--r--index.php93
-rw-r--r--tests/LinkDBTest.php33
-rw-r--r--tests/LinkFilterTest.php19
-rw-r--r--tests/UtilsTest.php112
-rw-r--r--tests/api/controllers/GetLinksTest.php4
-rw-r--r--tests/api/controllers/InfoTest.php4
-rw-r--r--tests/utils/ReferenceLinkDB.php26
-rw-r--r--tpl/default/changetag.html4
-rw-r--r--tpl/default/css/shaarli.css77
-rw-r--r--tpl/default/js/shaarli.js260
-rw-r--r--tpl/default/linklist.html11
-rw-r--r--tpl/default/page.footer.html3
-rw-r--r--tpl/default/page.header.html7
-rw-r--r--tpl/default/tag.cloud.html (renamed from tpl/default/tagcloud.html)24
-rw-r--r--tpl/default/tag.list.html86
-rw-r--r--tpl/default/tag.sort.html8
-rw-r--r--tpl/default/tools.html25
-rw-r--r--tpl/vintage/linklist.html6
-rw-r--r--tpl/vintage/tag.cloud.html (renamed from tpl/vintage/tagcloud.html)2
-rw-r--r--tpl/vintage/tools.html10
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 */
92function escape($input) 92function 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 */
453function 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}
diff --git a/index.php b/index.php
index 944af674..823eb8de 100644
--- a/index.php
+++ b/index.php
@@ -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)
1587function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) 1616function 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 */
5class ReferenceLinkDB 5class 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 */
530function findParent(element, tagName, attributes)
531{
532 while (element) {
533 if (element.tagName.toLowerCase() == tagName) {
534 var match = true;
535 for (var key in attributes) {
536 if (! element.hasAttribute(key)
537 || (attributes[key] != '' && element.getAttribute(key).indexOf(attributes[key]) == -1)
538 ) {
539 match = false;
540 break;
541 }
542 }
543
544 if (match) {
545 return element;
546 }
547 }
548 element = element.parentElement;
549 }
550 return null;
551}
552
553/**
554 * Ajax request to refresh the CSRF token.
555 */
556function refreshToken()
557{
558 var xhr = new XMLHttpRequest();
559 xhr.open('GET', '?do=token');
560 xhr.onload = function() {
561 var token = document.getElementById('token');
562 token.setAttribute('value', xhr.responseText);
563 };
564 xhr.send();
565}
566
567/**
568 * Update awesomplete list of tag for all elements matching the given selector
569 *
570 * @param selector CSS selector
571 * @param tags Array of tags
572 * @param instances List of existing awesomplete instances
573 */
574function updateAwesompleteList(selector, tags, instances)
575{
576 // First load: create Awesomplete instances
577 if (instances.length == 0) {
578 var elements = document.querySelectorAll(selector);
579 [].forEach.call(elements, function (element) {
580 instances.push(new Awesomplete(
581 element,
582 {'list': tags}
583 ));
584 });
585 } else {
586 // Update awesomplete tag list
587 for (var key in instances) {
588 instances[key].list = tags;
589 }
590 }
591 return instances;
592}
593
594/**
595 * html_entities in JS
596 *
597 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
598 */
599function htmlEntities(str)
600{
601 return str.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
602 return '&#'+i.charCodeAt(0)+';';
603 });
604}
605
367function activateFirefoxSocial(node) { 606function 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 */
398function hideTimezoneCities(cities, currentContinent, reset = false) { 637function 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>&nbsp;&nbsp;
47 <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
48 <i class="fa fa-pencil-square-o {$key}"></i>
49 </a>
50 {/if}
51
52 <a href="?addtag={$key|urlencode}" title="{'Filter by tag'|t}" class="count">{$value}</a>
53 <a href="?searchtags={$key|urlencode}" class="tag-link">{$key}</a>
54
55 {loop="$value.tag_plugin"}
56 {$value}
57 {/loop}
58 </div>
59 {if="isLoggedIn()===true"}
60 <div class="rename-tag-form pure-u-1">
61 <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
62 <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
63 </div>
64 {/if}
65 </div>
66 {/loop}
67 </div>
68
69 <div id="plugin_zone_end_tagcloud" class="plugin_zone">
70 {loop="$plugin_end_zone"}
71 {$value}
72 {/loop}
73 </div>
74 </div>
75</div>
76
77{if="isLoggedIn()===true"}
78 <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
79{/if}
80
81{include="tag.sort"}
82
83{include="page.footer"}
84</body>
85</html>
86
diff --git a/tpl/default/tag.sort.html b/tpl/default/tag.sort.html
new file mode 100644
index 00000000..89acda0d
--- /dev/null
+++ b/tpl/default/tag.sort.html
@@ -0,0 +1,8 @@
1<div class="pure-g">
2 <div class="pure-u-1 pure-alert pure-alert-success tag-sort">
3 {'Sort by:'|t}
4 <a href="?do=tagcloud" title="cloud">{'Cloud'|t}</a> &middot;
5 <a href="?do=taglist&sort=usage" title="cloud">{'Most used'|t}</a> &middot;
6 <a href="?do=taglist&sort=alpha" title="cloud">{'Alphabetical'|t}</a>
7 </div>
8</div> \ No newline at end of file
diff --git a/tpl/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 '&amp;title='%20+%20encodeURIComponent(title)+ 82 '&amp;title='%20+%20encodeURIComponent(title)+
78 '&amp;description='%20+%20encodeURIComponent(document.getSelection())+ 83 '&amp;description='%20+%20encodeURIComponent(desc)+
79 '&amp;source=bookmarklet','_blank','menubar=no,height=800,width=600,toolbar=no,scrollbars=yes,status=no,dialog=1' 84 '&amp;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&amp;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&amp;post='+
104 '&amp;description='%20+%20encodeURIComponent(desc)+
105 '&amp;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&amp;post="><b>✚Add Note</b></a> 42 href="javascript:(
43 function(){
44 window.open(
45 '{$pageabsaddr}?private=1&amp;post='+
46 '&amp;description='%20+%20encodeURIComponent(document.getSelection())+
47 '&amp;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 &#x21D0; Drag this link to your bookmarks toolbar (or right-click it and choose Bookmark This Link....).<br> 53 &#x21D0; Drag this link to your bookmarks toolbar (or right-click it and choose Bookmark This Link....).<br>