diff options
author | ArthurHoaro <arthur@hoa.ro> | 2017-03-12 19:03:50 +0100 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2017-05-08 14:27:20 +0200 |
commit | 29a837f347f53f751b723d466a2cd05fd92fd34e (patch) | |
tree | 5f477d220b7b8c1987b2b337dd69ceb26edc3f0b | |
parent | bf67ac345f588130e98e784b4ee4740b0dad83fc (diff) | |
download | Shaarli-29a837f347f53f751b723d466a2cd05fd92fd34e.tar.gz Shaarli-29a837f347f53f751b723d466a2cd05fd92fd34e.tar.zst Shaarli-29a837f347f53f751b723d466a2cd05fd92fd34e.zip |
Bulk deletion
* Add a checkboxes in linklist which display a sub-header containing action buttons
* Strongly rely on JS
* Requires a modern browser (ES6 syntax support)
* Checkboxes are hidden if the browser is old or JS disabled
-rw-r--r-- | index.php | 19 | ||||
-rw-r--r-- | tpl/default/css/shaarli.css | 13 | ||||
-rw-r--r-- | tpl/default/js/shaarli.js | 73 | ||||
-rw-r--r-- | tpl/default/linklist.html | 5 | ||||
-rw-r--r-- | tpl/default/page.header.html | 7 |
5 files changed, 106 insertions, 11 deletions
@@ -1329,18 +1329,21 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) | |||
1329 | // -------- User clicked the "Delete" button when editing a link: Delete link from database. | 1329 | // -------- User clicked the "Delete" button when editing a link: Delete link from database. |
1330 | if ($targetPage == Router::$PAGE_DELETELINK) | 1330 | if ($targetPage == Router::$PAGE_DELETELINK) |
1331 | { | 1331 | { |
1332 | // We do not need to ask for confirmation: | ||
1333 | // - confirmation is handled by JavaScript | ||
1334 | // - we are protected from XSRF by the token. | ||
1335 | |||
1336 | if (! tokenOk($_GET['token'])) { | 1332 | if (! tokenOk($_GET['token'])) { |
1337 | die('Wrong token.'); | 1333 | die('Wrong token.'); |
1338 | } | 1334 | } |
1339 | 1335 | ||
1340 | $id = intval(escape($_GET['lf_linkdate'])); | 1336 | if (strpos($_GET['lf_linkdate'], ' ') !== false) { |
1341 | $link = $LINKSDB[$id]; | 1337 | $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate'])))); |
1342 | $pluginManager->executeHooks('delete_link', $link); | 1338 | } else { |
1343 | unset($LINKSDB[$id]); | 1339 | $ids = [$_GET['lf_linkdate']]; |
1340 | } | ||
1341 | foreach ($ids as $id) { | ||
1342 | $id = (int) escape($id); | ||
1343 | $link = $LINKSDB[$id]; | ||
1344 | $pluginManager->executeHooks('delete_link', $link); | ||
1345 | unset($LINKSDB[$id]); | ||
1346 | } | ||
1344 | $LINKSDB->save($conf->get('resource.page_cache')); // save to disk | 1347 | $LINKSDB->save($conf->get('resource.page_cache')); // save to disk |
1345 | $history->deleteLink($link); | 1348 | $history->deleteLink($link); |
1346 | 1349 | ||
diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css index 73fade5f..efdf06d4 100644 --- a/tpl/default/css/shaarli.css +++ b/tpl/default/css/shaarli.css | |||
@@ -275,6 +275,19 @@ body, .pure-g [class*="pure-u"] { | |||
275 | } | 275 | } |
276 | } | 276 | } |
277 | 277 | ||
278 | .subheader-form a.button { | ||
279 | color: #f5f5f5; | ||
280 | font-weight: bold; | ||
281 | text-decoration: none; | ||
282 | border: 2px solid #f5f5f5; | ||
283 | border-radius: 5px; | ||
284 | padding: 3px 10px; | ||
285 | } | ||
286 | |||
287 | .linklist-item-editbuttons .delete-checkbox { | ||
288 | display: none; | ||
289 | } | ||
290 | |||
278 | #header-login-form input[type="text"], #header-login-form input[type="password"] { | 291 | #header-login-form input[type="text"], #header-login-form input[type="password"] { |
279 | width: 200px; | 292 | width: 200px; |
280 | } | 293 | } |
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js index 4d47fcd0..7abd20b2 100644 --- a/tpl/default/js/shaarli.js +++ b/tpl/default/js/shaarli.js | |||
@@ -357,11 +357,64 @@ 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 | * Note: Requires a modern browser. | ||
370 | */ | ||
371 | if (testEs6Compatibility()) { | ||
372 | let linkCheckboxes = document.querySelectorAll('.delete-checkbox'); | ||
373 | for(let checkbox of linkCheckboxes) { | ||
374 | checkbox.style.display = 'block'; | ||
375 | checkbox.addEventListener('click', function(event) { | ||
376 | let count = 0; | ||
377 | for(let checkbox of linkCheckboxes) { | ||
378 | count = checkbox.checked ? count + 1 : count; | ||
379 | } | ||
380 | let bar = document.getElementById('actions'); | ||
381 | if (count == 0 && bar.classList.contains('open')) { | ||
382 | bar.classList.toggle('open'); | ||
383 | } else if (count > 0 && ! bar.classList.contains('open')) { | ||
384 | bar.classList.toggle('open'); | ||
385 | } | ||
386 | }); | ||
387 | } | ||
388 | |||
389 | let deleteButton = document.getElementById('actions-delete'); | ||
390 | let token = document.querySelector('input[type="hidden"][name="token"]'); | ||
391 | if (deleteButton != null && token != null) { | ||
392 | deleteButton.addEventListener('click', function(event) { | ||
393 | event.preventDefault(); | ||
394 | |||
395 | let links = []; | ||
396 | for(let checkbox of linkCheckboxes) { | ||
397 | if (checkbox.checked) { | ||
398 | links.push({ | ||
399 | 'id': checkbox.value, | ||
400 | 'title': document.querySelector('.linklist-item[data-id="'+ checkbox.value +'"] .linklist-link').innerHTML | ||
401 | }); | ||
402 | } | ||
403 | } | ||
404 | |||
405 | let message = 'Are you sure you want to delete '+ links.length +' links?\n'; | ||
406 | message += 'This action is IRREVERSIBLE!\n\nTitles:\n'; | ||
407 | let ids = ''; | ||
408 | for (let item of links) { | ||
409 | message += ' - '+ item['title'] +'\n'; | ||
410 | ids += item['id'] +'+'; | ||
411 | } | ||
412 | if (window.confirm(message)) { | ||
413 | window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value; | ||
414 | } | ||
415 | }); | ||
416 | } | ||
417 | } | ||
365 | }; | 418 | }; |
366 | 419 | ||
367 | function activateFirefoxSocial(node) { | 420 | function activateFirefoxSocial(node) { |
@@ -397,7 +450,7 @@ function activateFirefoxSocial(node) { | |||
397 | */ | 450 | */ |
398 | function hideTimezoneCities(cities, currentContinent, reset = false) { | 451 | function hideTimezoneCities(cities, currentContinent, reset = false) { |
399 | var first = true; | 452 | var first = true; |
400 | [].forEach.call(cities, function(option) { | 453 | [].forEach.call(cities, function (option) { |
401 | if (option.getAttribute('data-continent') != currentContinent) { | 454 | if (option.getAttribute('data-continent') != currentContinent) { |
402 | option.className = 'hidden'; | 455 | option.className = 'hidden'; |
403 | } else { | 456 | } else { |
@@ -409,3 +462,19 @@ function hideTimezoneCities(cities, currentContinent, reset = false) { | |||
409 | } | 462 | } |
410 | }); | 463 | }); |
411 | } | 464 | } |
465 | |||
466 | /** | ||
467 | * Check if the browser is compatible with ECMAScript 6 syntax | ||
468 | * | ||
469 | * Source: http://stackoverflow.com/a/29046739/1484919 | ||
470 | * | ||
471 | * @returns {boolean} | ||
472 | */ | ||
473 | function testEs6Compatibility() | ||
474 | { | ||
475 | "use strict"; | ||
476 | |||
477 | try { eval("var foo = (x)=>x+1"); } | ||
478 | catch (e) { return false; } | ||
479 | return true; | ||
480 | } | ||
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index 57ef4567..6a4e14a6 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"> |
@@ -121,7 +123,7 @@ | |||
121 | <div class="pure-u-lg-20-24 pure-u-22-24"> | 123 | <div class="pure-u-lg-20-24 pure-u-22-24"> |
122 | {loop="links"} | 124 | {loop="links"} |
123 | <div class="anchor" id="{$value.shorturl}"></div> | 125 | <div class="anchor" id="{$value.shorturl}"></div> |
124 | <div class="linklist-item{if="$value.class"} {$value.class}{/if}"> | 126 | <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}"> |
125 | 127 | ||
126 | <div class="linklist-item-title"> | 128 | <div class="linklist-item-title"> |
127 | {if="isLoggedIn()"} | 129 | {if="isLoggedIn()"} |
@@ -129,6 +131,7 @@ | |||
129 | {if="$value.private"} | 131 | {if="$value.private"} |
130 | <span class="label label-private">{'Private'|t}</span> | 132 | <span class="label label-private">{'Private'|t}</span> |
131 | {/if} | 133 | {/if} |
134 | <input type="checkbox" class="delete-checkbox" value="{$value.id}"> | ||
132 | <!-- FIXME! JS translation --> | 135 | <!-- FIXME! JS translation --> |
133 | <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a> | 136 | <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> | 137 | <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a> |
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"> |