aboutsummaryrefslogtreecommitdiffhomepage
path: root/assets
diff options
context:
space:
mode:
Diffstat (limited to 'assets')
-rw-r--r--assets/common/js/metadata.js107
-rw-r--r--assets/common/js/shaare-batch.js121
-rw-r--r--assets/default/js/base.js73
-rw-r--r--assets/default/scss/shaarli.scss199
-rw-r--r--assets/vintage/css/shaarli.css61
-rw-r--r--assets/vintage/js/base.js45
6 files changed, 563 insertions, 43 deletions
diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js
new file mode 100644
index 00000000..d5a28a35
--- /dev/null
+++ b/assets/common/js/metadata.js
@@ -0,0 +1,107 @@
1import he from 'he';
2
3/**
4 * This script is used to retrieve bookmarks metadata asynchronously:
5 * - title, description and keywords while creating a new bookmark
6 * - thumbnails while visiting the bookmark list
7 *
8 * Note: it should only be included if the user is logged in
9 * and the setting general.enable_async_metadata is enabled.
10 */
11
12/**
13 * Removes given input loaders - used in edit link template.
14 *
15 * @param {object} loaders List of input DOM element that need to be cleared
16 */
17function clearLoaders(loaders) {
18 if (loaders != null && loaders.length > 0) {
19 [...loaders].forEach((loader) => {
20 loader.classList.remove('loading-input');
21 });
22 }
23}
24
25/**
26 * AJAX request to update the thumbnail of a bookmark with the provided ID.
27 * If a thumbnail is retrieved, it updates the divElement with the image src, and displays it.
28 *
29 * @param {string} basePath Shaarli subfolder for XHR requests
30 * @param {object} divElement Main <div> DOM element containing the thumbnail placeholder
31 * @param {int} id Bookmark ID to update
32 */
33function updateThumb(basePath, divElement, id) {
34 const xhr = new XMLHttpRequest();
35 xhr.open('PATCH', `${basePath}/admin/shaare/${id}/update-thumbnail`);
36 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
37 xhr.responseType = 'json';
38 xhr.onload = () => {
39 if (xhr.status !== 200) {
40 alert(`An error occurred. Return code: ${xhr.status}`);
41 } else {
42 const { response } = xhr;
43
44 if (response.thumbnail !== false) {
45 const imgElement = divElement.querySelector('img');
46
47 imgElement.src = response.thumbnail;
48 imgElement.dataset.src = response.thumbnail;
49 imgElement.style.opacity = '1';
50 divElement.classList.remove('hidden');
51 }
52 }
53 };
54 xhr.send();
55}
56
57(() => {
58 const basePath = document.querySelector('input[name="js_base_path"]').value;
59
60 /*
61 * METADATA FOR EDIT BOOKMARK PAGE
62 */
63 const inputTitles = document.querySelectorAll('input[name="lf_title"]');
64 if (inputTitles != null) {
65 [...inputTitles].forEach((inputTitle) => {
66 const form = inputTitle.closest('form[name="linkform"]');
67 const loaders = form.querySelectorAll('.loading-input');
68
69 if (inputTitle.value.length > 0) {
70 clearLoaders(loaders);
71 return;
72 }
73
74 const url = form.querySelector('input[name="lf_url"]').value;
75
76 const xhr = new XMLHttpRequest();
77 xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
78 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
79 xhr.onload = () => {
80 const result = JSON.parse(xhr.response);
81 Object.keys(result).forEach((key) => {
82 if (result[key] !== null && result[key].length) {
83 const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
84 if (element != null && element.value.length === 0) {
85 element.value = he.decode(result[key]);
86 }
87 }
88 });
89 clearLoaders(loaders);
90 };
91
92 xhr.send();
93 });
94 }
95
96 /*
97 * METADATA FOR THUMBNAIL RETRIEVAL
98 */
99 const thumbsToLoad = document.querySelectorAll('div[data-async-thumbnail]');
100 if (thumbsToLoad != null) {
101 [...thumbsToLoad].forEach((divElement) => {
102 const { id } = divElement.closest('[data-id]').dataset;
103
104 updateThumb(basePath, divElement, id);
105 });
106 }
107})();
diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js
new file mode 100644
index 00000000..557325ee
--- /dev/null
+++ b/assets/common/js/shaare-batch.js
@@ -0,0 +1,121 @@
1const sendBookmarkForm = (basePath, formElement) => {
2 const inputs = formElement
3 .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]');
4
5 const formData = new FormData();
6 [...inputs].forEach((input) => {
7 formData.append(input.getAttribute('name'), input.value);
8 });
9
10 return new Promise((resolve, reject) => {
11 const xhr = new XMLHttpRequest();
12 xhr.open('POST', `${basePath}/admin/shaare`);
13 xhr.onload = () => {
14 if (xhr.status !== 200) {
15 alert(`An error occurred. Return code: ${xhr.status}`);
16 reject();
17 } else {
18 formElement.closest('.edit-link-container').remove();
19 resolve();
20 }
21 };
22 xhr.send(formData);
23 });
24};
25
26const sendBookmarkDelete = (buttonElement, formElement) => (
27 new Promise((resolve, reject) => {
28 const xhr = new XMLHttpRequest();
29 xhr.open('GET', buttonElement.href);
30 xhr.onload = () => {
31 if (xhr.status !== 200) {
32 alert(`An error occurred. Return code: ${xhr.status}`);
33 reject();
34 } else {
35 formElement.closest('.edit-link-container').remove();
36 resolve();
37 }
38 };
39 xhr.send();
40 })
41);
42
43const redirectIfEmptyBatch = (basePath, formElements, path) => {
44 if (formElements == null || formElements.length === 0) {
45 window.location.href = `${basePath}${path}`;
46 }
47};
48
49(() => {
50 const basePath = document.querySelector('input[name="js_base_path"]').value;
51 const getForms = () => document.querySelectorAll('form[name="linkform"]');
52
53 const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]');
54 if (cancelButtons != null) {
55 [...cancelButtons].forEach((cancelButton) => {
56 cancelButton.addEventListener('click', (e) => {
57 e.preventDefault();
58 e.target.closest('form[name="linkform"]').remove();
59 redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare');
60 });
61 });
62 }
63
64 const saveButtons = document.querySelectorAll('[name="save_edit"]');
65 if (saveButtons != null) {
66 [...saveButtons].forEach((saveButton) => {
67 saveButton.addEventListener('click', (e) => {
68 e.preventDefault();
69
70 const formElement = e.target.closest('form[name="linkform"]');
71 sendBookmarkForm(basePath, formElement)
72 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
73 });
74 });
75 }
76
77 const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]');
78 if (saveAllButtons != null) {
79 [...saveAllButtons].forEach((saveAllButton) => {
80 saveAllButton.addEventListener('click', (e) => {
81 e.preventDefault();
82
83 const forms = [...getForms()];
84 const nbForm = forms.length;
85 let current = 0;
86 const progressBar = document.querySelector('.progressbar > div');
87 const progressBarCurrent = document.querySelector('.progressbar-current');
88
89 document.querySelector('.dark-layer').style.display = 'block';
90 document.querySelector('.progressbar-max').innerHTML = nbForm;
91 progressBarCurrent.innerHTML = current;
92
93 const promises = [];
94 forms.forEach((formElement) => {
95 promises.push(sendBookmarkForm(basePath, formElement).then(() => {
96 current += 1;
97 progressBar.style.width = `${(current * 100) / nbForm}%`;
98 progressBarCurrent.innerHTML = current;
99 }));
100 });
101
102 Promise.all(promises).then(() => {
103 window.location.href = basePath || '/';
104 });
105 });
106 });
107 }
108
109 const deleteButtons = document.querySelectorAll('[name="delete_link"]');
110 if (deleteButtons != null) {
111 [...deleteButtons].forEach((deleteButton) => {
112 deleteButton.addEventListener('click', (e) => {
113 e.preventDefault();
114
115 const formElement = e.target.closest('form[name="linkform"]');
116 sendBookmarkDelete(e.target, formElement)
117 .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
118 });
119 });
120 }
121})();
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index be986ae0..dd532bb7 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -1,4 +1,5 @@
1import Awesomplete from 'awesomplete'; 1import Awesomplete from 'awesomplete';
2import he from 'he';
2 3
3/** 4/**
4 * Find a parent element according to its tag and its attributes 5 * Find a parent element according to its tag and its attributes
@@ -41,19 +42,21 @@ function refreshToken(basePath, callback) {
41 xhr.send(); 42 xhr.send();
42} 43}
43 44
44function createAwesompleteInstance(element, tags = []) { 45function createAwesompleteInstance(element, separator, tags = []) {
45 const awesome = new Awesomplete(Awesomplete.$(element)); 46 const awesome = new Awesomplete(Awesomplete.$(element));
46 // Tags are separated by a space 47
47 awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); 48 // Tags are separated by separator
49 awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
48 // Insert new selected tag in the input 50 // Insert new selected tag in the input
49 awesome.replace = (text) => { 51 awesome.replace = (text) => {
50 const before = awesome.input.value.match(/^.+ \s*|/)[0]; 52 const before = awesome.input.value.match(new RegExp(`^.+${separator}+|`))[0];
51 awesome.input.value = `${before}${text} `; 53 awesome.input.value = `${before}${text}${separator}`;
52 }; 54 };
53 // Highlight found items 55 // Highlight found items
54 awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(/[^ ]*$/)[0]); 56 awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
55 // Don't display already selected items 57 // Don't display already selected items
56 const reg = /(\w+) /g; 58 // WARNING: pseudo classes does not seem to work with string litterals...
59 const reg = new RegExp(`([^${separator}]+)${separator}`, 'g');
57 let match; 60 let match;
58 awesome.data = (item, input) => { 61 awesome.data = (item, input) => {
59 while ((match = reg.exec(input))) { 62 while ((match = reg.exec(input))) {
@@ -77,13 +80,14 @@ function createAwesompleteInstance(element, tags = []) {
77 * @param selector CSS selector 80 * @param selector CSS selector
78 * @param tags Array of tags 81 * @param tags Array of tags
79 * @param instances List of existing awesomplete instances 82 * @param instances List of existing awesomplete instances
83 * @param separator Tags separator character
80 */ 84 */
81function updateAwesompleteList(selector, tags, instances) { 85function updateAwesompleteList(selector, tags, instances, separator) {
82 if (instances.length === 0) { 86 if (instances.length === 0) {
83 // First load: create Awesomplete instances 87 // First load: create Awesomplete instances
84 const elements = document.querySelectorAll(selector); 88 const elements = document.querySelectorAll(selector);
85 [...elements].forEach((element) => { 89 [...elements].forEach((element) => {
86 instances.push(createAwesompleteInstance(element, tags)); 90 instances.push(createAwesompleteInstance(element, separator, tags));
87 }); 91 });
88 } else { 92 } else {
89 // Update awesomplete tag list 93 // Update awesomplete tag list
@@ -96,15 +100,6 @@ function updateAwesompleteList(selector, tags, instances) {
96} 100}
97 101
98/** 102/**
99 * html_entities in JS
100 *
101 * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
102 */
103function htmlEntities(str) {
104 return str.replace(/[\u00A0-\u9999<>&]/gim, (i) => `&#${i.charCodeAt(0)};`);
105}
106
107/**
108 * Add the class 'hidden' to city options not attached to the current selected continent. 103 * Add the class 'hidden' to city options not attached to the current selected continent.
109 * 104 *
110 * @param cities List of <option> elements 105 * @param cities List of <option> elements
@@ -222,6 +217,8 @@ function init(description) {
222 217
223(() => { 218(() => {
224 const basePath = document.querySelector('input[name="js_base_path"]').value; 219 const basePath = document.querySelector('input[name="js_base_path"]').value;
220 const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
221 const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
225 222
226 /** 223 /**
227 * Handle responsive menu. 224 * Handle responsive menu.
@@ -302,7 +299,8 @@ function init(description) {
302 const deleteLinks = document.querySelectorAll('.confirm-delete'); 299 const deleteLinks = document.querySelectorAll('.confirm-delete');
303 [...deleteLinks].forEach((deleteLink) => { 300 [...deleteLinks].forEach((deleteLink) => {
304 deleteLink.addEventListener('click', (event) => { 301 deleteLink.addEventListener('click', (event) => {
305 if (!confirm(document.getElementById('translation-delete-link').innerHTML)) { 302 const type = event.currentTarget.getAttribute('data-type') || 'link';
303 if (!confirm(document.getElementById(`translation-delete-${type}`).innerHTML)) {
306 event.preventDefault(); 304 event.preventDefault();
307 } 305 }
308 }); 306 });
@@ -569,7 +567,7 @@ function init(description) {
569 input.setAttribute('name', totag); 567 input.setAttribute('name', totag);
570 input.setAttribute('value', totag); 568 input.setAttribute('value', totag);
571 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none'; 569 findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
572 block.querySelector('a.tag-link').innerHTML = htmlEntities(totag); 570 block.querySelector('a.tag-link').innerHTML = he.encode(totag);
573 block 571 block
574 .querySelector('a.tag-link') 572 .querySelector('a.tag-link')
575 .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`); 573 .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
@@ -582,7 +580,7 @@ function init(description) {
582 580
583 // Refresh awesomplete values 581 // Refresh awesomplete values
584 existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag)); 582 existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
585 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 583 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
586 } 584 }
587 }; 585 };
588 xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`); 586 xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
@@ -622,14 +620,14 @@ function init(description) {
622 refreshToken(basePath); 620 refreshToken(basePath);
623 621
624 existingTags = existingTags.filter((tagItem) => tagItem !== tag); 622 existingTags = existingTags.filter((tagItem) => tagItem !== tag);
625 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes); 623 awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
626 } 624 }
627 }); 625 });
628 }); 626 });
629 627
630 const autocompleteFields = document.querySelectorAll('input[data-multiple]'); 628 const autocompleteFields = document.querySelectorAll('input[data-multiple]');
631 [...autocompleteFields].forEach((autocompleteField) => { 629 [...autocompleteFields].forEach((autocompleteField) => {
632 awesomepletes.push(createAwesompleteInstance(autocompleteField)); 630 awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator));
633 }); 631 });
634 632
635 const exportForm = document.querySelector('#exportform'); 633 const exportForm = document.querySelector('#exportform');
@@ -642,4 +640,33 @@ function init(description) {
642 }); 640 });
643 }); 641 });
644 } 642 }
643
644 const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
645 if (bulkCreationButton != null) {
646 const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
647 if (bulkCreationButton.classList.contains('pure-u-0')) {
648 showMoreBlockElement.classList.remove('pure-u-0');
649 formElement.classList.add('pure-u-0');
650 } else {
651 showMoreBlockElement.classList.add('pure-u-0');
652 formElement.classList.remove('pure-u-0');
653 }
654 };
655
656 const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
657
658 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
659 bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
660 e.preventDefault();
661 toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
662 });
663
664 // Force to send falsy value if the checkbox is not checked.
665 const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
666 const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
667 privateButton.addEventListener('click', () => {
668 privateHiddenButton.disabled = !privateHiddenButton.disabled;
669 });
670 privateHiddenButton.disabled = privateButton.checked;
671 }
645})(); 672})();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index a528adb0..cc8ccc1e 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -139,6 +139,16 @@ body,
139 } 139 }
140} 140}
141 141
142.page-form,
143.pure-alert {
144 code {
145 display: inline-block;
146 padding: 0 2px;
147 color: $dark-grey;
148 background-color: var(--background-color);
149 }
150}
151
142// Make pure-extras alert closable. 152// Make pure-extras alert closable.
143.pure-alert-closable { 153.pure-alert-closable {
144 .fa-times { 154 .fa-times {
@@ -671,6 +681,10 @@ body,
671 content: ''; 681 content: '';
672 } 682 }
673 } 683 }
684
685 .search-highlight {
686 background-color: yellow;
687 }
674} 688}
675 689
676.linklist-item-buttons { 690.linklist-item-buttons {
@@ -1019,6 +1033,10 @@ body,
1019 &.button-red { 1033 &.button-red {
1020 background: $red; 1034 background: $red;
1021 } 1035 }
1036
1037 &.button-grey {
1038 background: $light-grey;
1039 }
1022 } 1040 }
1023 1041
1024 .submit-buttons { 1042 .submit-buttons {
@@ -1043,7 +1061,7 @@ body,
1043 } 1061 }
1044 1062
1045 table { 1063 table {
1046 margin: auto; 1064 margin: 10px auto 25px auto;
1047 width: 90%; 1065 width: 90%;
1048 1066
1049 .order { 1067 .order {
@@ -1079,6 +1097,11 @@ body,
1079 position: absolute; 1097 position: absolute;
1080 right: 5%; 1098 right: 5%;
1081 } 1099 }
1100
1101 &.button-grey {
1102 position: absolute;
1103 left: 5%;
1104 }
1082 } 1105 }
1083 } 1106 }
1084 } 1107 }
@@ -1253,11 +1276,15 @@ form {
1253 margin: 70px 0 25px; 1276 margin: 70px 0 25px;
1254 } 1277 }
1255 1278
1279 a {
1280 color: var(--main-color);
1281 }
1282
1256 pre { 1283 pre {
1257 margin: 0 20%; 1284 margin: 0 20%;
1258 padding: 20px 0; 1285 padding: 20px 0;
1259 text-align: left; 1286 text-align: left;
1260 line-height: .7em; 1287 line-height: 1em;
1261 } 1288 }
1262} 1289}
1263 1290
@@ -1269,6 +1296,57 @@ form {
1269 } 1296 }
1270} 1297}
1271 1298
1299.loading-input {
1300 position: relative;
1301
1302 @keyframes around {
1303 0% {
1304 transform: rotate(0deg);
1305 }
1306
1307 100% {
1308 transform: rotate(360deg);
1309 }
1310 }
1311
1312 .icon-container {
1313 position: absolute;
1314 right: 60px;
1315 top: calc(50% - 10px);
1316 }
1317
1318 .loader {
1319 position: relative;
1320 height: 20px;
1321 width: 20px;
1322 display: inline-block;
1323 animation: around 5.4s infinite;
1324
1325 &::after,
1326 &::before {
1327 content: "";
1328 background: $form-input-background;
1329 position: absolute;
1330 display: inline-block;
1331 width: 100%;
1332 height: 100%;
1333 border-width: 2px;
1334 border-color: #333 #333 transparent transparent;
1335 border-style: solid;
1336 border-radius: 20px;
1337 box-sizing: border-box;
1338 top: 0;
1339 left: 0;
1340 animation: around 0.7s ease-in-out infinite;
1341 }
1342
1343 &::after {
1344 animation: around 0.7s ease-in-out 0.1s infinite;
1345 background: transparent;
1346 }
1347 }
1348}
1349
1272// LOGIN 1350// LOGIN
1273.login-form-container { 1351.login-form-container {
1274 .remember-me { 1352 .remember-me {
@@ -1641,6 +1719,123 @@ form {
1641 } 1719 }
1642} 1720}
1643 1721
1722// SERVER PAGE
1723
1724.server-tables-page,
1725.server-tables {
1726 .window-subtitle {
1727 &::before {
1728 display: block;
1729 margin: 8px auto;
1730 background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color));
1731 width: 50%;
1732 height: 1px;
1733 content: '';
1734 }
1735 }
1736
1737 .server-row {
1738 p {
1739 height: 25px;
1740 padding: 0 10px;
1741 }
1742 }
1743
1744 .server-label {
1745 text-align: right;
1746 font-weight: bold;
1747 }
1748
1749 i {
1750 &.fa-color-green {
1751 color: $main-green;
1752 }
1753
1754 &.fa-color-orange {
1755 color: $orange;
1756 }
1757
1758 &.fa-color-red {
1759 color: $red;
1760 }
1761 }
1762
1763 @media screen and (max-width: 64em) {
1764 .server-label {
1765 text-align: center;
1766 }
1767
1768 .server-row {
1769 p {
1770 text-align: center;
1771 }
1772 }
1773 }
1774}
1775
1776// Batch creation
1777input[name='save_edit_batch'] {
1778 @extend %page-form-button;
1779}
1780
1781.addlink-batch-show-more {
1782 display: flex;
1783 align-items: center;
1784 margin: 20px 0 8px;
1785
1786 a {
1787 color: var(--main-color);
1788 text-decoration: none;
1789 }
1790
1791 &::before,
1792 &::after {
1793 content: "";
1794 flex-grow: 1;
1795 background: rgba(0, 0, 0, 0.35);
1796 height: 1px;
1797 font-size: 0;
1798 line-height: 0;
1799 }
1800
1801 &::before {
1802 margin: 0 16px 0 0;
1803 }
1804
1805 &::after {
1806 margin: 0 0 0 16px;
1807 }
1808}
1809
1810.dark-layer {
1811 display: none;
1812 position: fixed;
1813 height: 100%;
1814 width: 100%;
1815 z-index: 998;
1816 background-color: rgba(0, 0, 0, .75);
1817 color: #fff;
1818
1819 .screen-center {
1820 display: flex;
1821 flex-direction: column;
1822 justify-content: center;
1823 align-items: center;
1824 text-align: center;
1825 min-height: 100vh;
1826 }
1827
1828 .progressbar {
1829 width: 33%;
1830 }
1831}
1832
1833.addlink-batch-form-block {
1834 .pure-alert {
1835 margin: 25px 0 0 0;
1836 }
1837}
1838
1644// Print rules 1839// Print rules
1645@media print { 1840@media print {
1646 .shaarli-menu { 1841 .shaarli-menu {
diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css
index 1688dce0..33e178af 100644
--- a/assets/vintage/css/shaarli.css
+++ b/assets/vintage/css/shaarli.css
@@ -1122,6 +1122,16 @@ ul.errors {
1122 float: left; 1122 float: left;
1123} 1123}
1124 1124
1125ul.warnings {
1126 color: orange;
1127 float: left;
1128}
1129
1130ul.successes {
1131 color: green;
1132 float: left;
1133}
1134
1125#pluginsadmin { 1135#pluginsadmin {
1126 width: 80%; 1136 width: 80%;
1127 padding: 20px 0 0 20px; 1137 padding: 20px 0 0 20px;
@@ -1248,3 +1258,54 @@ ul.errors {
1248 width: 0%; 1258 width: 0%;
1249 height: 10px; 1259 height: 10px;
1250} 1260}
1261
1262.loading-input {
1263 position: relative;
1264}
1265
1266@keyframes around {
1267 0% {
1268 transform: rotate(0deg);
1269 }
1270
1271 100% {
1272 transform: rotate(360deg);
1273 }
1274}
1275
1276.loading-input .icon-container {
1277 position: absolute;
1278 right: 60px;
1279 top: calc(50% - 10px);
1280}
1281
1282.loading-input .loader {
1283 position: relative;
1284 height: 20px;
1285 width: 20px;
1286 display: inline-block;
1287 animation: around 5.4s infinite;
1288}
1289
1290.loading-input .loader::after,
1291.loading-input .loader::before {
1292 content: "";
1293 background: #eee;
1294 position: absolute;
1295 display: inline-block;
1296 width: 100%;
1297 height: 100%;
1298 border-width: 2px;
1299 border-color: #333 #333 transparent transparent;
1300 border-style: solid;
1301 border-radius: 20px;
1302 box-sizing: border-box;
1303 top: 0;
1304 left: 0;
1305 animation: around 0.7s ease-in-out infinite;
1306}
1307
1308.loading-input .loader::after {
1309 animation: around 0.7s ease-in-out 0.1s infinite;
1310 background: transparent;
1311}
diff --git a/assets/vintage/js/base.js b/assets/vintage/js/base.js
index 66830b59..55f1c37d 100644
--- a/assets/vintage/js/base.js
+++ b/assets/vintage/js/base.js
@@ -2,29 +2,38 @@ import Awesomplete from 'awesomplete';
2import 'awesomplete/awesomplete.css'; 2import 'awesomplete/awesomplete.css';
3 3
4(() => { 4(() => {
5 const awp = Awesomplete.$;
6 const autocompleteFields = document.querySelectorAll('input[data-multiple]'); 5 const autocompleteFields = document.querySelectorAll('input[data-multiple]');
6 const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
7 const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
8
7 [...autocompleteFields].forEach((autocompleteField) => { 9 [...autocompleteFields].forEach((autocompleteField) => {
8 const awesomplete = new Awesomplete(awp(autocompleteField)); 10 const awesome = new Awesomplete(Awesomplete.$(autocompleteField));
9 awesomplete.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); 11
10 awesomplete.replace = (text) => { 12 // Tags are separated by separator
11 const before = awesomplete.input.value.match(/^.+ \s*|/)[0]; 13 awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(
12 awesomplete.input.value = `${before}${text} `; 14 text,
15 input.match(new RegExp(`[^${tagsSeparator}]*$`))[0],
16 );
17 // Insert new selected tag in the input
18 awesome.replace = (text) => {
19 const before = awesome.input.value.match(new RegExp(`^.+${tagsSeparator}+|`))[0];
20 awesome.input.value = `${before}${text}${tagsSeparator}`;
13 }; 21 };
14 awesomplete.minChars = 1; 22 // Highlight found items
23 awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]);
15 24
16 autocompleteField.addEventListener('input', () => { 25 // Don't display already selected items
17 const proposedTags = autocompleteField.getAttribute('data-list').replace(/,/g, '').split(' '); 26 // WARNING: pseudo classes does not seem to work with string litterals...
18 const reg = /(\w+) /g; 27 const reg = new RegExp(`([^${tagsSeparator}]+)${tagsSeparator}`, 'g');
19 let match; 28 let match;
20 while ((match = reg.exec(autocompleteField.value)) !== null) { 29 awesome.data = (item, input) => {
21 const id = proposedTags.indexOf(match[1]); 30 while ((match = reg.exec(input))) {
22 if (id !== -1) { 31 if (item === match[1]) {
23 proposedTags.splice(id, 1); 32 return '';
24 } 33 }
25 } 34 }
26 35 return item;
27 awesomplete.list = proposedTags; 36 };
28 }); 37 awesome.minChars = 1;
29 }); 38 });
30})(); 39})();