From 5d8de7587d67b5c3e5d1fed8562d9b87ecde80c1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 10 Oct 2020 17:40:26 +0200 Subject: Feature: bulk creation of bookmarks This changes creates a new form in addlink page allowing to create multiple bookmarks at once more easily. It focuses on re-using as much existing code and template component as possible. These changes includes: - a new form in addlink (hidden behind a button by default), containing a text area for URL, and tags/private status to apply to created links - this form displays a new template called editlink.batch, itself including editlink template multiple times - User interation in this new templates are handle by a new JS script (shaare-batch.js) making AJAX requests, and therefore does not need page reloading - ManageShaareController has been split into 3 distinct controllers: + ShaareAdd: displays addlink template + ShaareManage: various operation applied on existing shaares (change visibility, pin, deletion, etc.) + ShaarePublish: handles creation/edit forms and saving Shaare's form - Updated translations Fixes #137 --- assets/common/js/metadata.js | 50 +++++++++--------- assets/common/js/shaare-batch.js | 107 +++++++++++++++++++++++++++++++++++++++ assets/default/js/base.js | 21 ++++++++ assets/default/scss/shaarli.scss | 49 ++++++++++++++++++ 4 files changed, 204 insertions(+), 23 deletions(-) create mode 100644 assets/common/js/shaare-batch.js (limited to 'assets') diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js index 2b013364..d5a28a35 100644 --- a/assets/common/js/metadata.js +++ b/assets/common/js/metadata.js @@ -56,37 +56,41 @@ function updateThumb(basePath, divElement, id) { (() => { const basePath = document.querySelector('input[name="js_base_path"]').value; - const loaders = document.querySelectorAll('.loading-input'); /* * METADATA FOR EDIT BOOKMARK PAGE */ - const inputTitle = document.querySelector('input[name="lf_title"]'); - if (inputTitle != null) { - if (inputTitle.value.length > 0) { - clearLoaders(loaders); - return; - } + const inputTitles = document.querySelectorAll('input[name="lf_title"]'); + if (inputTitles != null) { + [...inputTitles].forEach((inputTitle) => { + const form = inputTitle.closest('form[name="linkform"]'); + const loaders = form.querySelectorAll('.loading-input'); + + if (inputTitle.value.length > 0) { + clearLoaders(loaders); + return; + } - const url = document.querySelector('input[name="lf_url"]').value; + const url = form.querySelector('input[name="lf_url"]').value; - const xhr = new XMLHttpRequest(); - xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onload = () => { - const result = JSON.parse(xhr.response); - Object.keys(result).forEach((key) => { - if (result[key] !== null && result[key].length) { - const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); - if (element != null && element.value.length === 0) { - element.value = he.decode(result[key]); + const xhr = new XMLHttpRequest(); + xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.onload = () => { + const result = JSON.parse(xhr.response); + Object.keys(result).forEach((key) => { + if (result[key] !== null && result[key].length) { + const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`); + if (element != null && element.value.length === 0) { + element.value = he.decode(result[key]); + } } - } - }); - clearLoaders(loaders); - }; + }); + clearLoaders(loaders); + }; - xhr.send(); + xhr.send(); + }); } /* diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js new file mode 100644 index 00000000..9f612993 --- /dev/null +++ b/assets/common/js/shaare-batch.js @@ -0,0 +1,107 @@ +const sendBookmarkForm = (basePath, formElement) => { + const inputs = formElement + .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]'); + + const formData = new FormData(); + [...inputs].forEach((input) => { + formData.append(input.getAttribute('name'), input.value); + }); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${basePath}/admin/shaare`); + xhr.onload = () => { + if (xhr.status !== 200) { + alert(`An error occurred. Return code: ${xhr.status}`); + reject(); + } else { + formElement.remove(); + resolve(); + } + }; + xhr.send(formData); + }); +}; + +const sendBookmarkDelete = (buttonElement, formElement) => ( + new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', buttonElement.href); + xhr.onload = () => { + if (xhr.status !== 200) { + alert(`An error occurred. Return code: ${xhr.status}`); + reject(); + } else { + formElement.remove(); + resolve(); + } + }; + xhr.send(); + }) +); + +const redirectIfEmptyBatch = (basePath, formElements, path) => { + if (formElements == null || formElements.length === 0) { + window.location.href = `${basePath}${path}`; + } +}; + +(() => { + const basePath = document.querySelector('input[name="js_base_path"]').value; + const getForms = () => document.querySelectorAll('form[name="linkform"]'); + + const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]'); + if (cancelButtons != null) { + [...cancelButtons].forEach((cancelButton) => { + cancelButton.addEventListener('click', (e) => { + e.preventDefault(); + e.target.closest('form[name="linkform"]').remove(); + redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare'); + }); + }); + } + + const saveButtons = document.querySelectorAll('[name="save_edit"]'); + if (saveButtons != null) { + [...saveButtons].forEach((saveButton) => { + saveButton.addEventListener('click', (e) => { + e.preventDefault(); + + const formElement = e.target.closest('form[name="linkform"]'); + sendBookmarkForm(basePath, formElement) + .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); + }); + }); + } + + const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]'); + if (saveAllButtons != null) { + [...saveAllButtons].forEach((saveAllButton) => { + saveAllButton.addEventListener('click', (e) => { + e.preventDefault(); + + const promises = []; + [...getForms()].forEach((formElement) => { + promises.push(sendBookmarkForm(basePath, formElement)); + }); + + Promise.all(promises).then(() => { + window.location.href = basePath || '/'; + }); + }); + }); + } + + const deleteButtons = document.querySelectorAll('[name="delete_link"]'); + if (deleteButtons != null) { + [...deleteButtons].forEach((deleteButton) => { + deleteButton.addEventListener('click', (e) => { + e.preventDefault(); + + const formElement = e.target.closest('form[name="linkform"]'); + sendBookmarkDelete(e.target, formElement) + .then(() => redirectIfEmptyBatch(basePath, getForms(), '/')); + }); + }); + } +})(); diff --git a/assets/default/js/base.js b/assets/default/js/base.js index 7f6b9637..9161b4fc 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -634,4 +634,25 @@ function init(description) { }); }); } + + 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); + }); + } })(); diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index 7dc61903..7c85dee8 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1023,6 +1023,10 @@ body, &.button-red { background: $red; } + + &.button-grey { + background: $light-grey; + } } .submit-buttons { @@ -1083,6 +1087,11 @@ body, position: absolute; right: 5%; } + + &.button-grey { + position: absolute; + left: 5%; + } } } } @@ -1750,6 +1759,46 @@ form { } } +// Batch creation +input[name='save_edit_batch'] { + @extend %page-form-button; +} + +.addlink-batch-show-more { + display: flex; + align-items: center; + margin: 20px 0 8px; + + a { + color: var(--main-color); + text-decoration: none; + } + + &::before, + &::after { + content: ""; + flex-grow: 1; + background: rgba(0, 0, 0, 0.35); + height: 1px; + font-size: 0; + line-height: 0; + } + + &::before { + margin: 0 16px 0 0; + } + + &::after { + margin: 0 0 0 16px; + } +} + +.addlink-batch-form-block { + .pure-alert { + margin: 25px 0 0 0; + } +} + // Print rules @media print { .shaarli-menu { -- cgit v1.2.3 From 25e90d8d75382721ff7473fa1686090fcfeb46ff Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 11 Oct 2020 13:34:38 +0200 Subject: Bulk creation: fix private status based on the first form --- assets/default/js/base.js | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'assets') diff --git a/assets/default/js/base.js b/assets/default/js/base.js index 9161b4fc..4163577d 100644 --- a/assets/default/js/base.js +++ b/assets/default/js/base.js @@ -654,5 +654,13 @@ function init(description) { 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; } })(); -- cgit v1.2.3 From 6a716758871885e5bd045d1981f890dbf3343b1d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 12:53:49 +0200 Subject: Bulk creation: displays a progress bar when saving all displayed forms --- assets/common/js/shaare-batch.js | 22 ++++++++++++++++++---- assets/default/scss/shaarli.scss | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) (limited to 'assets') diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js index 9f612993..557325ee 100644 --- a/assets/common/js/shaare-batch.js +++ b/assets/common/js/shaare-batch.js @@ -15,7 +15,7 @@ const sendBookmarkForm = (basePath, formElement) => { alert(`An error occurred. Return code: ${xhr.status}`); reject(); } else { - formElement.remove(); + formElement.closest('.edit-link-container').remove(); resolve(); } }; @@ -32,7 +32,7 @@ const sendBookmarkDelete = (buttonElement, formElement) => ( alert(`An error occurred. Return code: ${xhr.status}`); reject(); } else { - formElement.remove(); + formElement.closest('.edit-link-container').remove(); resolve(); } }; @@ -80,9 +80,23 @@ const redirectIfEmptyBatch = (basePath, formElements, path) => { saveAllButton.addEventListener('click', (e) => { e.preventDefault(); + const forms = [...getForms()]; + const nbForm = forms.length; + let current = 0; + const progressBar = document.querySelector('.progressbar > div'); + const progressBarCurrent = document.querySelector('.progressbar-current'); + + document.querySelector('.dark-layer').style.display = 'block'; + document.querySelector('.progressbar-max').innerHTML = nbForm; + progressBarCurrent.innerHTML = current; + const promises = []; - [...getForms()].forEach((formElement) => { - promises.push(sendBookmarkForm(basePath, formElement)); + forms.forEach((formElement) => { + promises.push(sendBookmarkForm(basePath, formElement).then(() => { + current += 1; + progressBar.style.width = `${(current * 100) / nbForm}%`; + progressBarCurrent.innerHTML = current; + })); }); Promise.all(promises).then(() => { diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss index 7c85dee8..a7f091e9 100644 --- a/assets/default/scss/shaarli.scss +++ b/assets/default/scss/shaarli.scss @@ -1793,6 +1793,29 @@ input[name='save_edit_batch'] { } } +.dark-layer { + display: none; + position: fixed; + height: 100%; + width: 100%; + z-index: 998; + background-color: rgba(0, 0, 0, .75); + color: #fff; + + .screen-center { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + min-height: 100vh; + } + + .progressbar { + width: 33%; + } +} + .addlink-batch-form-block { .pure-alert { margin: 25px 0 0 0; -- cgit v1.2.3