]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - assets/default/js/base.js
ESLint
[github/shaarli/Shaarli.git] / assets / default / js / base.js
index 5cf037c2c99a6ad620a86bee4a88447d1c0a6b12..dd532bb71bb40bbd4de82afe0b2255ab69c40826 100644 (file)
@@ -1,4 +1,5 @@
 import Awesomplete from 'awesomplete';
+import he from 'he';
 
 /**
  * Find a parent element according to its tag and its attributes
@@ -10,7 +11,7 @@ import Awesomplete from 'awesomplete';
  * @returns Found element or null.
  */
 function findParent(element, tagName, attributes) {
-  const parentMatch = key => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
+  const parentMatch = (key) => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
   while (element) {
     if (element.tagName.toLowerCase() === tagName) {
       if (Object.keys(attributes).find(parentMatch)) {
@@ -25,29 +26,37 @@ function findParent(element, tagName, attributes) {
 /**
  * Ajax request to refresh the CSRF token.
  */
-function refreshToken() {
+function refreshToken(basePath, callback) {
   const xhr = new XMLHttpRequest();
-  xhr.open('GET', '?do=token');
+  xhr.open('GET', `${basePath}/admin/token`);
   xhr.onload = () => {
-    const token = document.getElementById('token');
-    token.setAttribute('value', xhr.responseText);
+    const elements = document.querySelectorAll('input[name="token"]');
+    [...elements].forEach((element) => {
+      element.setAttribute('value', xhr.responseText);
+    });
+
+    if (callback) {
+      callback(xhr.response);
+    }
   };
   xhr.send();
 }
 
-function createAwesompleteInstance(element, tags = []) {
+function createAwesompleteInstance(element, separator, tags = []) {
   const awesome = new Awesomplete(Awesomplete.$(element));
-  // Tags are separated by a space
-  awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
+
+  // Tags are separated by separator
+  awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
   // Insert new selected tag in the input
   awesome.replace = (text) => {
-    const before = awesome.input.value.match(/^.+ \s*|/)[0];
-    awesome.input.value = `${before}${text} `;
+    const before = awesome.input.value.match(new RegExp(`^.+${separator}+|`))[0];
+    awesome.input.value = `${before}${text}${separator}`;
   };
   // Highlight found items
-  awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(/[^ ]*$/)[0]);
+  awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
   // Don't display already selected items
-  const reg = /(\w+) /g;
+  // WARNING: pseudo classes does not seem to work with string litterals...
+  const reg = new RegExp(`([^${separator}]+)${separator}`, 'g');
   let match;
   awesome.data = (item, input) => {
     while ((match = reg.exec(input))) {
@@ -71,13 +80,14 @@ function createAwesompleteInstance(element, tags = []) {
  * @param selector  CSS selector
  * @param tags      Array of tags
  * @param instances List of existing awesomplete instances
+ * @param separator Tags separator character
  */
-function updateAwesompleteList(selector, tags, instances) {
+function updateAwesompleteList(selector, tags, instances, separator) {
   if (instances.length === 0) {
     // First load: create Awesomplete instances
     const elements = document.querySelectorAll(selector);
     [...elements].forEach((element) => {
-      instances.push(createAwesompleteInstance(element, tags));
+      instances.push(createAwesompleteInstance(element, separator, tags));
     });
   } else {
     // Update awesomplete tag list
@@ -89,38 +99,6 @@ function updateAwesompleteList(selector, tags, instances) {
   return instances;
 }
 
-/**
- * html_entities in JS
- *
- * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
- */
-function htmlEntities(str) {
-  return str.replace(/[\u00A0-\u9999<>&]/gim, i => `&#${i.charCodeAt(0)};`);
-}
-
-function activateFirefoxSocial(node) {
-  const loc = location.href;
-  const baseURL = loc.substring(0, loc.lastIndexOf('/') + 1);
-
-  const data = {
-    name: document.title,
-    description: document.getElementById('translation-delete-link').innerHTML,
-    author: 'Shaarli',
-    version: '1.0.0',
-
-    iconURL: `${baseURL}/images/favicon.ico`,
-    icon32URL: `${baseURL}/images/favicon.ico`,
-    icon64URL: `${baseURL}/images/favicon.ico`,
-
-    shareURL: `${baseURL}?post=%{url}&title=%{title}&description=%{text}&source=firefoxsocialapi`,
-    homepageURL: baseURL,
-  };
-  node.setAttribute('data-service', JSON.stringify(data));
-
-  const activate = new CustomEvent('ActivateSocialFeature');
-  node.dispatchEvent(activate);
-}
-
 /**
  * Add the class 'hidden' to city options not attached to the current selected continent.
  *
@@ -211,8 +189,8 @@ function removeClass(element, classname) {
 function init(description) {
   function resize() {
     /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
-    const scrollTop = window.pageYOffset ||
-      (document.documentElement || document.body.parentNode || document.body).scrollTop;
+    const scrollTop = window.pageYOffset
+      || (document.documentElement || document.body.parentNode || document.body).scrollTop;
 
     description.style.height = 'auto';
     description.style.height = `${description.scrollHeight + 10}px`;
@@ -238,6 +216,10 @@ function init(description) {
 }
 
 (() => {
+  const basePath = document.querySelector('input[name="js_base_path"]').value;
+  const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
+  const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
+
   /**
    * Handle responsive menu.
    * Source: http://purecss.io/layouts/tucked-menu-vertical/
@@ -317,7 +299,8 @@ function init(description) {
   const deleteLinks = document.querySelectorAll('.confirm-delete');
   [...deleteLinks].forEach((deleteLink) => {
     deleteLink.addEventListener('click', (event) => {
-      if (!confirm(document.getElementById('translation-delete-link').innerHTML)) {
+      const type = event.currentTarget.getAttribute('data-type') || 'link';
+      if (!confirm(document.getElementById(`translation-delete-${type}`).innerHTML)) {
         event.preventDefault();
       }
     });
@@ -433,16 +416,6 @@ function init(description) {
     });
   });
 
-  /**
-   * Firefox Social
-   */
-  const ffButton = document.getElementById('ff-social-button');
-  if (ffButton != null) {
-    ffButton.addEventListener('click', (event) => {
-      activateFirefoxSocial(event.target);
-    });
-  }
-
   const continent = document.getElementById('continent');
   const city = document.getElementById('city');
   if (continent != null && city != null) {
@@ -455,12 +428,12 @@ function init(description) {
   /**
    * Bulk actions
    */
-  const linkCheckboxes = document.querySelectorAll('.delete-checkbox');
+  const linkCheckboxes = document.querySelectorAll('.link-checkbox');
   const bar = document.getElementById('actions');
   [...linkCheckboxes].forEach((checkbox) => {
     checkbox.style.display = 'inline-block';
-    checkbox.addEventListener('click', () => {
-      const linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
+    checkbox.addEventListener('change', () => {
+      const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
       const count = [...linkCheckedCheckboxes].length;
       if (count === 0 && bar.classList.contains('open')) {
         bar.classList.toggle('open');
@@ -477,7 +450,7 @@ function init(description) {
       event.preventDefault();
 
       const links = [];
-      const linkCheckedCheckboxes = document.querySelectorAll('.delete-checkbox:checked');
+      const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
       [...linkCheckedCheckboxes].forEach((checkbox) => {
         links.push({
           id: checkbox.value,
@@ -494,11 +467,54 @@ function init(description) {
       });
 
       if (window.confirm(message)) {
-        window.location = `?delete_link&lf_linkdate=${ids.join('+')}&token=${token.value}`;
+        window.location = `${basePath}/admin/shaare/delete?id=${ids.join('+')}&token=${token.value}`;
       }
     });
   }
 
+  const changeVisibilityButtons = document.querySelectorAll('.actions-change-visibility');
+  if (changeVisibilityButtons != null && token != null) {
+    [...changeVisibilityButtons].forEach((button) => {
+      button.addEventListener('click', (event) => {
+        event.preventDefault();
+        const visibility = event.target.getAttribute('data-visibility');
+
+        const links = [];
+        const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
+        [...linkCheckedCheckboxes].forEach((checkbox) => {
+          links.push({
+            id: checkbox.value,
+            title: document.querySelector(`.linklist-item[data-id="${checkbox.value}"] .linklist-link`).innerHTML,
+          });
+        });
+
+        const ids = links.map((item) => item.id);
+        window.location = (
+          `${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`
+        );
+      });
+    });
+  }
+
+  /**
+   * Select all button
+   */
+  const selectAllButtons = document.querySelectorAll('.select-all-button');
+  [...selectAllButtons].forEach((selectAllButton) => {
+    selectAllButton.addEventListener('click', (e) => {
+      e.preventDefault();
+      const checked = selectAllButton.classList.contains('filter-off');
+      [...selectAllButtons].forEach((selectAllButton2) => {
+        selectAllButton2.classList.toggle('filter-off');
+        selectAllButton2.classList.toggle('filter-on');
+      });
+      [...linkCheckboxes].forEach((linkCheckbox) => {
+        linkCheckbox.checked = checked;
+        linkCheckbox.dispatchEvent(new Event('change'));
+      });
+    });
+  });
+
   /**
    * Tag list operations
    *
@@ -537,8 +553,9 @@ function init(description) {
       }
       const refreshedToken = document.getElementById('token').value;
       const fromtag = block.getAttribute('data-tag');
+      const fromtagUrl = block.getAttribute('data-tag-url');
       const xhr = new XMLHttpRequest();
-      xhr.open('POST', '?do=changetag');
+      xhr.open('POST', `${basePath}/admin/tags`);
       xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
       xhr.onload = () => {
         if (xhr.status !== 200) {
@@ -546,20 +563,28 @@ function init(description) {
           location.reload();
         } else {
           block.setAttribute('data-tag', totag);
+          block.setAttribute('data-tag-url', encodeURIComponent(totag));
           input.setAttribute('name', totag);
           input.setAttribute('value', totag);
           findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
-          block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
-          block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`);
-          block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`);
+          block.querySelector('a.tag-link').innerHTML = he.encode(totag);
+          block
+            .querySelector('a.tag-link')
+            .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
+          block
+            .querySelector('a.count')
+            .setAttribute('href', `${basePath}/add-tag/${encodeURIComponent(totag)}`);
+          block
+            .querySelector('a.rename-tag')
+            .setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
 
           // Refresh awesomplete values
-          existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
-          awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
+          existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
+          awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
         }
       };
-      xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
-      refreshToken();
+      xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
+      refreshToken(basePath);
     });
   });
 
@@ -581,26 +606,67 @@ function init(description) {
       event.preventDefault();
       const block = findParent(event.target, 'div', { class: 'tag-list-item' });
       const tag = block.getAttribute('data-tag');
-      const refreshedToken = document.getElementById('token');
+      const tagUrl = block.getAttribute('data-tag-url');
+      const refreshedToken = document.getElementById('token').value;
 
       if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
         const xhr = new XMLHttpRequest();
-        xhr.open('POST', '?do=changetag');
+        xhr.open('POST', `${basePath}/admin/tags`);
         xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
         xhr.onload = () => {
           block.remove();
         };
-        xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
-        refreshToken();
+        xhr.send(`deletetag=1&fromtag=${tagUrl}&token=${refreshedToken}`);
+        refreshToken(basePath);
 
-        existingTags = existingTags.filter(tagItem => tagItem !== tag);
-        awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
+        existingTags = existingTags.filter((tagItem) => tagItem !== tag);
+        awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
       }
     });
   });
 
   const autocompleteFields = document.querySelectorAll('input[data-multiple]');
   [...autocompleteFields].forEach((autocompleteField) => {
-    awesomepletes.push(createAwesompleteInstance(autocompleteField));
+    awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator));
   });
+
+  const exportForm = document.querySelector('#exportform');
+  if (exportForm != null) {
+    exportForm.addEventListener('submit', (event) => {
+      event.preventDefault();
+
+      refreshToken(basePath, () => {
+        event.target.submit();
+      });
+    });
+  }
+
+  const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
+  if (bulkCreationButton != null) {
+    const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
+      if (bulkCreationButton.classList.contains('pure-u-0')) {
+        showMoreBlockElement.classList.remove('pure-u-0');
+        formElement.classList.add('pure-u-0');
+      } else {
+        showMoreBlockElement.classList.add('pure-u-0');
+        formElement.classList.remove('pure-u-0');
+      }
+    };
+
+    const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
+
+    toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
+    bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
+      e.preventDefault();
+      toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
+    });
+
+    // Force to send falsy value if the checkbox is not checked.
+    const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
+    const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
+    privateButton.addEventListener('click', () => {
+      privateHiddenButton.disabled = !privateHiddenButton.disabled;
+    });
+    privateHiddenButton.disabled = privateButton.checked;
+  }
 })();