diff options
Diffstat (limited to 'assets')
-rw-r--r-- | assets/common/js/metadata.js | 107 | ||||
-rw-r--r-- | assets/common/js/shaare-batch.js | 121 | ||||
-rw-r--r-- | assets/default/js/base.js | 73 | ||||
-rw-r--r-- | assets/default/scss/shaarli.scss | 199 | ||||
-rw-r--r-- | assets/vintage/css/shaarli.css | 61 | ||||
-rw-r--r-- | assets/vintage/js/base.js | 45 |
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 @@ | |||
1 | import 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 | */ | ||
17 | function 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 | */ | ||
33 | function 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 @@ | |||
1 | const 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 | |||
26 | const 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 | |||
43 | const 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 @@ | |||
1 | import Awesomplete from 'awesomplete'; | 1 | import Awesomplete from 'awesomplete'; |
2 | import 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 | ||
44 | function createAwesompleteInstance(element, tags = []) { | 45 | function 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 | */ |
81 | function updateAwesompleteList(selector, tags, instances) { | 85 | function 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 | */ | ||
103 | function 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 | ||
1777 | input[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 | ||
1125 | ul.warnings { | ||
1126 | color: orange; | ||
1127 | float: left; | ||
1128 | } | ||
1129 | |||
1130 | ul.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'; | |||
2 | import 'awesomplete/awesomplete.css'; | 2 | import '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 | })(); |