aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2017-03-12 19:03:50 +0100
committerArthurHoaro <arthur@hoa.ro>2017-05-08 14:27:20 +0200
commit29a837f347f53f751b723d466a2cd05fd92fd34e (patch)
tree5f477d220b7b8c1987b2b337dd69ceb26edc3f0b
parentbf67ac345f588130e98e784b4ee4740b0dad83fc (diff)
downloadShaarli-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.php19
-rw-r--r--tpl/default/css/shaarli.css13
-rw-r--r--tpl/default/js/shaarli.js73
-rw-r--r--tpl/default/linklist.html5
-rw-r--r--tpl/default/page.header.html7
5 files changed, 106 insertions, 11 deletions
diff --git a/index.php b/index.php
index ab1e30da..5e61cbb0 100644
--- a/index.php
+++ b/index.php
@@ -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
367function activateFirefoxSocial(node) { 420function activateFirefoxSocial(node) {
@@ -397,7 +450,7 @@ function activateFirefoxSocial(node) {
397 */ 450 */
398function hideTimezoneCities(cities, currentContinent, reset = false) { 451function 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 */
473function 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">