]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Bulk deletion
authorArthurHoaro <arthur@hoa.ro>
Sun, 12 Mar 2017 18:03:50 +0000 (19:03 +0100)
committerArthurHoaro <arthur@hoa.ro>
Mon, 8 May 2017 12:27:20 +0000 (14:27 +0200)
  * 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

index.php
tpl/default/css/shaarli.css
tpl/default/js/shaarli.js
tpl/default/linklist.html
tpl/default/page.header.html

index ab1e30da9154c91c610014a93eafff882a7a01d9..5e61cbb00f4095af34883310f4354fb269b9e204 100644 (file)
--- a/index.php
+++ b/index.php
@@ -1329,18 +1329,21 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
     // -------- User clicked the "Delete" button when editing a link: Delete link from database.
     if ($targetPage == Router::$PAGE_DELETELINK)
     {
-        // We do not need to ask for confirmation:
-        // - confirmation is handled by JavaScript
-        // - we are protected from XSRF by the token.
-
         if (! tokenOk($_GET['token'])) {
             die('Wrong token.');
         }
 
-        $id = intval(escape($_GET['lf_linkdate']));
-        $link = $LINKSDB[$id];
-        $pluginManager->executeHooks('delete_link', $link);
-        unset($LINKSDB[$id]);
+        if (strpos($_GET['lf_linkdate'], ' ') !== false) {
+            $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate']))));
+        } else {
+            $ids = [$_GET['lf_linkdate']];
+        }
+        foreach ($ids as $id) {
+            $id = (int) escape($id);
+            $link = $LINKSDB[$id];
+            $pluginManager->executeHooks('delete_link', $link);
+            unset($LINKSDB[$id]);
+        }
         $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
         $history->deleteLink($link);
 
index 73fade5ffa3a4a6570ecb296baa7e51391f0f3df..efdf06d4d35a677b9ad4b155c978de16e8e53426 100644 (file)
@@ -275,6 +275,19 @@ body, .pure-g [class*="pure-u"] {
     }
 }
 
+.subheader-form a.button {
+    color: #f5f5f5;
+    font-weight: bold;
+    text-decoration: none;
+    border: 2px solid #f5f5f5;
+    border-radius: 5px;
+    padding: 3px 10px;
+}
+
+.linklist-item-editbuttons .delete-checkbox {
+    display: none;
+}
+
 #header-login-form input[type="text"], #header-login-form input[type="password"] {
     width: 200px;
 }
index 4d47fcd0c2cd4aaf3be8080e84c61782adabd247..7abd20b24fc61c54b1359d0c08456c35d51ba612 100644 (file)
@@ -357,11 +357,64 @@ window.onload = function () {
     var continent = document.getElementById('continent');
     var city = document.getElementById('city');
     if (continent != null && city != null) {
-        continent.addEventListener('change', function(event) {
+        continent.addEventListener('change', function (event) {
             hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
         });
         hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
     }
+
+    /**
+     * Bulk actions
+     *
+     * Note: Requires a modern browser.
+     */
+    if (testEs6Compatibility()) {
+        let linkCheckboxes = document.querySelectorAll('.delete-checkbox');
+        for(let checkbox of linkCheckboxes) {
+            checkbox.style.display = 'block';
+            checkbox.addEventListener('click', function(event) {
+                let count = 0;
+                for(let checkbox of linkCheckboxes) {
+                    count = checkbox.checked ? count + 1 : count;
+                }
+                let bar = document.getElementById('actions');
+                if (count == 0 && bar.classList.contains('open')) {
+                    bar.classList.toggle('open');
+                } else if (count > 0 && ! bar.classList.contains('open')) {
+                    bar.classList.toggle('open');
+                }
+            });
+        }
+
+        let deleteButton = document.getElementById('actions-delete');
+        let token = document.querySelector('input[type="hidden"][name="token"]');
+        if (deleteButton != null && token != null) {
+            deleteButton.addEventListener('click', function(event) {
+                event.preventDefault();
+
+                let links = [];
+                for(let checkbox of linkCheckboxes) {
+                    if (checkbox.checked) {
+                        links.push({
+                            'id': checkbox.value,
+                            'title': document.querySelector('.linklist-item[data-id="'+ checkbox.value +'"] .linklist-link').innerHTML
+                        });
+                    }
+                }
+
+                let message = 'Are you sure you want to delete '+ links.length +' links?\n';
+                message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
+                let ids = '';
+                for (let item of links) {
+                    message += '  - '+ item['title'] +'\n';
+                    ids += item['id'] +'+';
+                }
+                if (window.confirm(message)) {
+                    window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value;
+                }
+            });
+        }
+    }
 };
 
 function activateFirefoxSocial(node) {
@@ -397,7 +450,7 @@ function activateFirefoxSocial(node) {
  */
 function hideTimezoneCities(cities, currentContinent, reset = false) {
     var first = true;
-    [].forEach.call(cities, function(option) {
+    [].forEach.call(cities, function (option) {
         if (option.getAttribute('data-continent') != currentContinent) {
             option.className = 'hidden';
         } else {
@@ -409,3 +462,19 @@ function hideTimezoneCities(cities, currentContinent, reset = false) {
         }
     });
 }
+
+/**
+ * Check if the browser is compatible with ECMAScript 6 syntax
+ *
+ * Source: http://stackoverflow.com/a/29046739/1484919
+ *
+ * @returns {boolean}
+ */
+function testEs6Compatibility()
+{
+    "use strict";
+
+    try { eval("var foo = (x)=>x+1"); }
+    catch (e) { return false; }
+    return true;
+}
index 57ef4567a8ee754c1f308c3b153e6d0188223b86..6a4e14a6a820b2bf0629b07ab312c642fee2abac 100644 (file)
@@ -15,6 +15,8 @@
   {/if}
 </div>
 
+<input type="hidden" name="token" value="{$token}">
+
 <div id="search-linklist">
 
   <div class="pure-g">
     <div class="pure-u-lg-20-24 pure-u-22-24">
       {loop="links"}
         <div class="anchor" id="{$value.shorturl}"></div>
-        <div class="linklist-item{if="$value.class"} {$value.class}{/if}">
+        <div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
 
           <div class="linklist-item-title">
             {if="isLoggedIn()"}
                 {if="$value.private"}
                   <span class="label label-private">{'Private'|t}</span>
                 {/if}
+                <input type="checkbox" class="delete-checkbox" value="{$value.id}">
                 <!-- FIXME! JS translation -->
                 <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a>
                 <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
index 9388ef79e9cdc42ca3e1b9c288a21df91d4255ce..6c71a718371a3f93a010a2e246dbe16fd810a930 100644 (file)
       </div>
     </div>
   </div>
+  <div id="actions" class="subheader-form">
+    <div class="pure-g">
+      <div class="pure-u-1">
+        <a href="" id="actions-delete" class="button">Delete</a>
+      </div>
+    </div>
+  </div>
   {if="!isLoggedIn()"}
     <form method="post" name="loginform">
       <div class="subheader-form" id="header-login-form">