diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..b2cd33f8
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+**/vendor/**
diff --git a/.eslintrc.js b/.eslintrc.js
index d39859f1..b65fe988 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -6,5 +6,85 @@ module.exports = {
"es6": true
},
- "extends": "eslint:recommended"
+ "extends": "eslint:recommended",
+
+ "rules": {
+ // Possible Errors
+ "no-async-promise-executor": "error",
+ "no-await-in-loop": "error",
+ "no-class-assign": "error",
+ "no-confusing-arrow": "error",
+ "no-const-assign": "error",
+ "no-dupe-class-members": "error",
+ "no-duplicate-imports": "error",
+ "no-template-curly-in-string": "error",
+ "no-useless-computed-key": "error",
+ "no-useless-constructor": "error",
+ "no-useless-rename": "error",
+ "require-atomic-updates": "error",
+
+ // Best practices
+ "strict": "error",
+ "no-var": "error",
+
+ // Stylistic Issues
+ "arrow-spacing": "error",
+ "capitalized-comments": [
+ "warn",
+ "always",
+ {
+ "ignoreConsecutiveComments": true
+ },
+ ],
+ "keyword-spacing": "error",
+ "lines-around-comment": [
+ "error",
+ {
+ "beforeBlockComment": true,
+ "beforeLineComment": true,
+ "allowBlockStart": true,
+ "allowClassStart": true,
+ "allowObjectStart": true,
+ "allowArrayStart": true,
+ },
+ ],
+ "no-multiple-empty-lines": [
+ "error",
+ {
+ "max": 1,
+ },
+ ],
+ "padded-blocks": [
+ "error",
+ "never",
+ ],
+ "padding-line-between-statements": [
+ "error",
+ {
+ // always before return
+ "blankLine": "always",
+ "prev": "*",
+ "next": "return",
+ },
+ {
+ // always before block-like expressions
+ "blankLine": "always",
+ "prev": "*",
+ "next": "block-like",
+ },
+ {
+ // always after variable declaration
+ "blankLine": "always",
+ "prev": [ "const", "let", "var" ],
+ "next": "*",
+ },
+ {
+ // not necessary between variable declaration
+ "blankLine": "any",
+ "prev": [ "const", "let", "var" ],
+ "next": [ "const", "let", "var" ],
+ },
+ ],
+ "space-before-blocks": "error",
+ }
};
diff --git a/.github/workflows/lint-frontend.yaml b/.github/workflows/lint-frontend.yaml
index 978bbbbe..f370d83b 100644
--- a/.github/workflows/lint-frontend.yaml
+++ b/.github/workflows/lint-frontend.yaml
@@ -3,12 +3,14 @@ name: Lint Frontend
on:
push:
- branches: [ main, ci ]
+ branches: [ main, ci, frontend ]
paths:
- '.github/workflows/**'
- 'static/**'
+ - '.eslintrc'
+ - '.stylelintrc'
pull_request:
- branches: [ main, ci ]
+ branches: [ main, ci, frontend ]
jobs:
lint:
@@ -22,8 +24,10 @@ jobs:
- name: Install modules
run: yarn
+ # See .stylelintignore for files that are not linted.
- name: Run stylelint
- run: yarn stylelint **/static/**/*.css --report-needless-disables --report-invalid-scope-disables
+ run: yarn stylelint bookwyrm/static/**/*.css --report-needless-disables --report-invalid-scope-disables
+ # See .eslintignore for files that are not linted.
- name: Run ESLint
- run: yarn eslint . --ext .js,.jsx,.ts,.tsx
+ run: yarn eslint bookwyrm/static --ext .js,.jsx,.ts,.tsx
diff --git a/.stylelintignore b/.stylelintignore
index f456cb22..b2cd33f8 100644
--- a/.stylelintignore
+++ b/.stylelintignore
@@ -1,2 +1 @@
-bookwyrm/static/css/bulma.*.css*
-bookwyrm/static/css/icons.css
+**/vendor/**
diff --git a/README.md b/README.md
index dc3af920..6dad4178 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,8 @@ If you edit the CSS or JavaScript, you will need to run Django's `collectstatic`
./bw-dev collectstatic
```
+If you have [installed yarn](https://yarnpkg.com/getting-started/install), you can run `yarn watch:static` to automatically run the previous script every time a change occurs in _bookwyrm/static_ directory.
+
### Working with translations and locale files
Text in the html files are wrapped in translation tags (`{% trans %}` and `{% blocktrans %}`), and Django generates locale files for all the strings in which you can add translations for the text. You can find existing translations in the `locale/` directory.
diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/bookwyrm.css
similarity index 73%
rename from bookwyrm/static/css/format.css
rename to bookwyrm/static/css/bookwyrm.css
index a01aff82..e4bca203 100644
--- a/bookwyrm/static/css/format.css
+++ b/bookwyrm/static/css/bookwyrm.css
@@ -3,7 +3,6 @@ html {
scroll-padding-top: 20%;
}
-/* --- --- */
.image {
overflow: hidden;
}
@@ -25,17 +24,8 @@ html {
min-width: 75% !important;
}
-/* --- "disabled" for non-buttons --- */
-.is-disabled {
- background-color: #dbdbdb;
- border-color: #dbdbdb;
- box-shadow: none;
- color: #7a7a7a;
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* --- SHELVING --- */
+/** Shelving
+ ******************************************************************************/
/** @todo Replace icons with SVG symbols.
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
@@ -45,7 +35,9 @@ html {
margin-left: 0.5em;
}
-/* --- TOGGLES --- */
+/** Toggles
+ ******************************************************************************/
+
.toggle-button[aria-pressed=true],
.toggle-button[aria-pressed=true]:hover {
background-color: hsl(171, 100%, 41%);
@@ -57,12 +49,8 @@ html {
display: none;
}
-.hidden {
- display: none !important;
-}
-
-.hidden.transition-y,
-.hidden.transition-x {
+.transition-x.is-hidden,
+.transition-y.is-hidden {
display: block !important;
visibility: hidden !important;
height: 0;
@@ -71,16 +59,18 @@ html {
padding: 0;
}
+.transition-x,
.transition-y {
- transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom;
transition-duration: 0.5s;
transition-timing-function: ease;
}
.transition-x {
transition-property: width, margin-left, margin-right, padding-left, padding-right;
- transition-duration: 0.5s;
- transition-timing-function: ease;
+}
+
+.transition-y {
+ transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom;
}
@media (prefers-reduced-motion: reduce) {
@@ -121,7 +111,9 @@ html {
content: '\e9d7';
}
-/* --- BOOK COVERS --- */
+/** Book covers
+ ******************************************************************************/
+
.cover-container {
height: 250px;
width: max-content;
@@ -186,7 +178,9 @@ html {
padding: 0.1em;
}
-/* --- AVATAR --- */
+/** Avatars
+ ******************************************************************************/
+
.avatar {
vertical-align: middle;
display: inline;
@@ -202,25 +196,57 @@ html {
min-height: 96px;
}
-/* --- QUOTES --- */
-.quote blockquote {
+/** Statuses: Quotes
+ *
+ * \e906: icon-quote-open
+ * \e905: icon-quote-close
+ *
+ * The `content` class on the blockquote allows to apply styles to markdown
+ * generated HTML in the quote: https://bulma.io/documentation/elements/content/
+ *
+ * ```html
+ *
+ *
+ * User generated quote in markdown…
+ *
+ *
+ *
— Book Title by Author
+ *
+ * ```
+ ******************************************************************************/
+
+.quote > blockquote {
position: relative;
padding-left: 2em;
}
-.quote blockquote::before,
-.quote blockquote::after {
+.quote > blockquote::before,
+.quote > blockquote::after {
font-family: 'icomoon';
position: absolute;
}
-.quote blockquote::before {
+.quote > blockquote::before {
content: "\e906";
top: 0;
left: 0;
}
-.quote blockquote::after {
+.quote > blockquote::after {
content: "\e905";
right: 0;
}
+
+/* States
+ ******************************************************************************/
+
+/* "disabled" for non-buttons */
+
+.is-disabled {
+ background-color: #dbdbdb;
+ border-color: #dbdbdb;
+ box-shadow: none;
+ color: #7a7a7a;
+ opacity: 0.5;
+ cursor: not-allowed;
+}
diff --git a/bookwyrm/static/css/bulma.css.map b/bookwyrm/static/css/vendor/bulma.css.map
similarity index 100%
rename from bookwyrm/static/css/bulma.css.map
rename to bookwyrm/static/css/vendor/bulma.css.map
diff --git a/bookwyrm/static/css/bulma.min.css b/bookwyrm/static/css/vendor/bulma.min.css
similarity index 100%
rename from bookwyrm/static/css/bulma.min.css
rename to bookwyrm/static/css/vendor/bulma.min.css
diff --git a/bookwyrm/static/css/icons.css b/bookwyrm/static/css/vendor/icons.css
similarity index 86%
rename from bookwyrm/static/css/icons.css
rename to bookwyrm/static/css/vendor/icons.css
index 9915ecd1..c78af145 100644
--- a/bookwyrm/static/css/icons.css
+++ b/bookwyrm/static/css/vendor/icons.css
@@ -1,10 +1,13 @@
+
+/** @todo Replace icons with SVG symbols.
+ @see https://www.youtube.com/watch?v=9xXBYcWgCHA */
@font-face {
font-family: 'icomoon';
- src: url('fonts/icomoon.eot?n5x55');
- src: url('fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'),
- url('fonts/icomoon.ttf?n5x55') format('truetype'),
- url('fonts/icomoon.woff?n5x55') format('woff'),
- url('fonts/icomoon.svg?n5x55#icomoon') format('svg');
+ src: url('../fonts/icomoon.eot?n5x55');
+ src: url('../fonts/icomoon.eot?n5x55#iefix') format('embedded-opentype'),
+ url('../fonts/icomoon.ttf?n5x55') format('truetype'),
+ url('../fonts/icomoon.woff?n5x55') format('woff'),
+ url('../fonts/icomoon.svg?n5x55#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js
new file mode 100644
index 00000000..485daf15
--- /dev/null
+++ b/bookwyrm/static/js/bookwyrm.js
@@ -0,0 +1,285 @@
+/* exported BookWyrm */
+/* globals TabGroup */
+
+let BookWyrm = new class {
+ constructor() {
+ this.initOnDOMLoaded();
+ this.initReccuringTasks();
+ this.initEventListeners();
+ }
+
+ initEventListeners() {
+ document.querySelectorAll('[data-controls]')
+ .forEach(button => button.addEventListener(
+ 'click',
+ this.toggleAction.bind(this))
+ );
+
+ document.querySelectorAll('.interaction')
+ .forEach(button => button.addEventListener(
+ 'submit',
+ this.interact.bind(this))
+ );
+
+ document.querySelectorAll('.hidden-form input')
+ .forEach(button => button.addEventListener(
+ 'change',
+ this.revealForm.bind(this))
+ );
+
+ document.querySelectorAll('[data-back]')
+ .forEach(button => button.addEventListener(
+ 'click',
+ this.back)
+ );
+ }
+
+ /**
+ * Execute code once the DOM is loaded.
+ */
+ initOnDOMLoaded() {
+ window.addEventListener('DOMContentLoaded', function() {
+ document.querySelectorAll('.tab-group')
+ .forEach(tabs => new TabGroup(tabs));
+ });
+ }
+
+ /**
+ * Execute recurring tasks.
+ */
+ initReccuringTasks() {
+ // Polling
+ document.querySelectorAll('[data-poll]')
+ .forEach(liveArea => this.polling(liveArea));
+ }
+
+ /**
+ * Go back in browser history.
+ *
+ * @param {Event} event
+ * @return {undefined}
+ */
+ back(event) {
+ event.preventDefault();
+ history.back();
+ }
+
+ /**
+ * Update a counter with recurring requests to the API
+ * The delay is slightly randomized and increased on each cycle.
+ *
+ * @param {Object} counter - DOM node
+ * @param {int} delay - frequency for polling in ms
+ * @return {undefined}
+ */
+ polling(counter, delay) {
+ const bookwyrm = this;
+
+ delay = delay || 10000;
+ delay += (Math.random() * 1000);
+
+ setTimeout(function() {
+ fetch('/api/updates/' + counter.dataset.poll)
+ .then(response => response.json())
+ .then(data => bookwyrm.updateCountElement(counter, data));
+
+ bookwyrm.polling(counter, delay * 1.25);
+ }, delay, counter);
+ }
+
+ /**
+ * Update a counter.
+ *
+ * @param {object} counter - DOM node
+ * @param {object} data - json formatted response from a fetch
+ * @return {undefined}
+ */
+ updateCountElement(counter, data) {
+ const currentCount = counter.innerText;
+ const count = data.count;
+
+ if (count != currentCount) {
+ this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
+ counter.innerText = count;
+ }
+ }
+
+ /**
+ * Toggle form.
+ *
+ * @param {Event} event
+ * @return {undefined}
+ */
+ revealForm(event) {
+ let trigger = event.currentTarget;
+ let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0];
+
+ this.addRemoveClass(hidden, 'is-hidden', !hidden);
+ }
+
+ /**
+ * Execute actions on targets based on triggers.
+ *
+ * @param {Event} event
+ * @return {undefined}
+ */
+ toggleAction(event) {
+ let trigger = event.currentTarget;
+ let pressed = trigger.getAttribute('aria-pressed') === 'false';
+ let targetId = trigger.dataset.controls;
+
+ // Toggle pressed status on all triggers controlling the same target.
+ document.querySelectorAll('[data-controls="' + targetId + '"]')
+ .forEach(otherTrigger => otherTrigger.setAttribute(
+ 'aria-pressed',
+ otherTrigger.getAttribute('aria-pressed') === 'false'
+ ));
+
+ // @todo Find a better way to handle the exception.
+ if (targetId && ! trigger.classList.contains('pulldown-menu')) {
+ let target = document.getElementById(targetId);
+
+ this.addRemoveClass(target, 'is-hidden', !pressed);
+ this.addRemoveClass(target, 'is-active', pressed);
+ }
+
+ // Show/hide pulldown-menus.
+ if (trigger.classList.contains('pulldown-menu')) {
+ this.toggleMenu(trigger, targetId);
+ }
+
+ // Show/hide container.
+ let container = document.getElementById('hide-' + targetId);
+
+ if (container) {
+ this.toggleContainer(container, pressed);
+ }
+
+ // Check checkbox, if appropriate.
+ let checkbox = trigger.dataset.controlsCheckbox;
+
+ if (checkbox) {
+ this.toggleCheckbox(checkbox, pressed);
+ }
+
+ // Set focus, if appropriate.
+ let focus = trigger.dataset.focusTarget;
+
+ if (focus) {
+ this.toggleFocus(focus);
+ }
+ }
+
+ /**
+ * Show or hide menus.
+ *
+ * @param {Event} event
+ * @return {undefined}
+ */
+ toggleMenu(trigger, targetId) {
+ let expanded = trigger.getAttribute('aria-expanded') == 'false';
+
+ trigger.setAttribute('aria-expanded', expanded);
+
+ if (targetId) {
+ let target = document.getElementById(targetId);
+
+ this.addRemoveClass(target, 'is-active', expanded);
+ }
+ }
+
+ /**
+ * Show or hide generic containers.
+ *
+ * @param {object} container - DOM node
+ * @param {boolean} pressed - Is the trigger pressed?
+ * @return {undefined}
+ */
+ toggleContainer(container, pressed) {
+ this.addRemoveClass(container, 'is-hidden', pressed);
+ }
+
+ /**
+ * Check or uncheck a checbox.
+ *
+ * @param {object} checkbox - DOM node
+ * @param {boolean} pressed - Is the trigger pressed?
+ * @return {undefined}
+ */
+ toggleCheckbox(checkbox, pressed) {
+ document.getElementById(checkbox).checked = !!pressed;
+ }
+
+ /**
+ * Give the focus to an element.
+ * Only move the focus based on user interactions.
+ *
+ * @param {string} nodeId - ID of the DOM node to focus (button, link…)
+ * @return {undefined}
+ */
+ toggleFocus(nodeId) {
+ let node = document.getElementById(nodeId);
+
+ node.focus();
+
+ setTimeout(function() {
+ node.selectionStart = node.selectionEnd = 10000;
+ }, 0);
+ }
+
+ /**
+ * Make a request and update the UI accordingly.
+ * This function is used for boosts, favourites, follows and unfollows.
+ *
+ * @param {Event} event
+ * @return {undefined}
+ */
+ interact(event) {
+ event.preventDefault();
+
+ const bookwyrm = this;
+ const form = event.currentTarget;
+ const relatedforms = document.querySelectorAll(`.${form.dataset.id}`);
+
+ // Toggle class on all related forms.
+ relatedforms.forEach(relatedForm => bookwyrm.addRemoveClass(
+ relatedForm,
+ 'is-hidden',
+ relatedForm.className.indexOf('is-hidden') == -1
+ ));
+
+ this.ajaxPost(form).catch(error => {
+ // @todo Display a notification in the UI instead.
+ console.warn('Request failed:', error);
+ });
+ }
+
+ /**
+ * Submit a form using POST.
+ *
+ * @param {object} form - Form to be submitted
+ * @return {Promise}
+ */
+ ajaxPost(form) {
+ return fetch(form.action, {
+ method : "POST",
+ body: new FormData(form)
+ });
+ }
+
+ /**
+ * Add or remove a class based on a boolean condition.
+ *
+ * @param {object} node - DOM node to change class on
+ * @param {string} classname - Name of the class
+ * @param {boolean} add - Add?
+ * @return {undefined}
+ */
+ addRemoveClass(node, classname, add) {
+ if (add) {
+ node.classList.add(classname);
+ } else {
+ node.classList.remove(classname);
+ }
+ }
+}
diff --git a/bookwyrm/static/js/check_all.js b/bookwyrm/static/js/check_all.js
index 07d30a68..fd29f2cd 100644
--- a/bookwyrm/static/js/check_all.js
+++ b/bookwyrm/static/js/check_all.js
@@ -1,17 +1,34 @@
-/* exported toggleAllCheckboxes */
-/**
- * Toggle all descendant checkboxes of a target.
- *
- * Use `data-target="ID_OF_TARGET"` on the node being listened to.
- *
- * @param {Event} event - change Event
- * @return {undefined}
- */
-function toggleAllCheckboxes(event) {
- const mainCheckbox = event.target;
+(function() {
+ 'use strict';
+
+ /**
+ * Toggle all descendant checkboxes of a target.
+ *
+ * Use `data-target="ID_OF_TARGET"` on the node on which the event is listened
+ * to (checkbox, button, link…), where_ID_OF_TARGET_ should be the ID of an
+ * ancestor for the checkboxes.
+ *
+ * @example
+ *
+ * @param {Event} event
+ * @return {undefined}
+ */
+ function toggleAllCheckboxes(event) {
+ const mainCheckbox = event.target;
+
+ document
+ .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`)
+ .forEach(checkbox => checkbox.checked = mainCheckbox.checked);
+ }
document
- .querySelectorAll(`#${mainCheckbox.dataset.target} [type="checkbox"]`)
- .forEach(checkbox => {checkbox.checked = mainCheckbox.checked;});
-}
+ .querySelectorAll('[data-action="toggle-all"]')
+ .forEach(input => {
+ input.addEventListener('change', toggleAllCheckboxes);
+ });
+})();
diff --git a/bookwyrm/static/js/localstorage.js b/bookwyrm/static/js/localstorage.js
index aa79ee30..05955779 100644
--- a/bookwyrm/static/js/localstorage.js
+++ b/bookwyrm/static/js/localstorage.js
@@ -1,20 +1,43 @@
-/* exported updateDisplay */
-/* globals addRemoveClass */
+/* exported LocalStorageTools */
+/* globals BookWyrm */
-// set javascript listeners
-function updateDisplay(e) {
- // used in set reading goal
- var key = e.target.getAttribute('data-id');
- var value = e.target.getAttribute('data-value');
- window.localStorage.setItem(key, value);
+let LocalStorageTools = new class {
+ constructor() {
+ document.querySelectorAll('[data-hide]')
+ .forEach(t => this.setDisplay(t));
- document.querySelectorAll('[data-hide="' + key + '"]')
- .forEach(t => setDisplay(t));
-}
-
-function setDisplay(el) {
- // used in set reading goal
- var key = el.getAttribute('data-hide');
- var value = window.localStorage.getItem(key);
- addRemoveClass(el, 'hidden', value);
+ document.querySelectorAll('.set-display')
+ .forEach(t => t.addEventListener('click', this.updateDisplay.bind(this)));
+ }
+
+ /**
+ * Update localStorage, then display content based on keys in localStorage.
+ *
+ * @param {Event} event
+ * @return {undefined}
+ */
+ updateDisplay(event) {
+ // used in set reading goal
+ let key = event.target.dataset.id;
+ let value = event.target.dataset.value;
+
+ window.localStorage.setItem(key, value);
+
+ document.querySelectorAll('[data-hide="' + key + '"]')
+ .forEach(node => this.setDisplay(node));
+ }
+
+ /**
+ * Toggle display of a DOM node based on its value in the localStorage.
+ *
+ * @param {object} node - DOM node to toggle.
+ * @return {undefined}
+ */
+ setDisplay(node) {
+ // used in set reading goal
+ let key = node.dataset.hide;
+ let value = window.localStorage.getItem(key);
+
+ BookWyrm.addRemoveClass(node, 'is-hidden', value);
+ }
}
diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js
deleted file mode 100644
index 7a198619..00000000
--- a/bookwyrm/static/js/shared.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/* globals setDisplay TabGroup toggleAllCheckboxes updateDisplay */
-
-// set up javascript listeners
-window.onload = function() {
- // buttons that display or hide content
- document.querySelectorAll('[data-controls]')
- .forEach(t => t.onclick = toggleAction);
-
- // javascript interactions (boost/fav)
- Array.from(document.getElementsByClassName('interaction'))
- .forEach(t => t.onsubmit = interact);
-
- // handle aria settings on menus
- Array.from(document.getElementsByClassName('pulldown-menu'))
- .forEach(t => t.onclick = toggleMenu);
-
- // hidden submit button in a form
- document.querySelectorAll('.hidden-form input')
- .forEach(t => t.onchange = revealForm);
-
- // polling
- document.querySelectorAll('[data-poll]')
- .forEach(el => polling(el));
-
- // browser back behavior
- document.querySelectorAll('[data-back]')
- .forEach(t => t.onclick = back);
-
- Array.from(document.getElementsByClassName('tab-group'))
- .forEach(t => new TabGroup(t));
-
- // display based on localstorage vars
- document.querySelectorAll('[data-hide]')
- .forEach(t => setDisplay(t));
-
- // update localstorage
- Array.from(document.getElementsByClassName('set-display'))
- .forEach(t => t.onclick = updateDisplay);
-
- // Toggle all checkboxes.
- document
- .querySelectorAll('[data-action="toggle-all"]')
- .forEach(input => {
- input.addEventListener('change', toggleAllCheckboxes);
- });
-};
-
-function back(e) {
- e.preventDefault();
- history.back();
-}
-
-function polling(el, delay) {
- delay = delay || 10000;
- delay += (Math.random() * 1000);
- setTimeout(function() {
- fetch('/api/updates/' + el.getAttribute('data-poll'))
- .then(response => response.json())
- .then(data => updateCountElement(el, data));
- polling(el, delay * 1.25);
- }, delay, el);
-}
-
-function updateCountElement(el, data) {
- const currentCount = el.innerText;
- const count = data.count;
- if (count != currentCount) {
- addRemoveClass(el.closest('[data-poll-wrapper]'), 'hidden', count < 1);
- el.innerText = count;
- }
-}
-
-
-function revealForm(e) {
- var hidden = e.currentTarget.closest('.hidden-form').getElementsByClassName('hidden')[0];
- if (hidden) {
- removeClass(hidden, 'hidden');
- }
-}
-
-
-function toggleAction(e) {
- var el = e.currentTarget;
- var pressed = el.getAttribute('aria-pressed') == 'false';
-
- var targetId = el.getAttribute('data-controls');
- document.querySelectorAll('[data-controls="' + targetId + '"]')
- .forEach(t => t.setAttribute('aria-pressed', (t.getAttribute('aria-pressed') == 'false')));
-
- if (targetId) {
- var target = document.getElementById(targetId);
- addRemoveClass(target, 'hidden', !pressed);
- addRemoveClass(target, 'is-active', pressed);
- }
-
- // show/hide container
- var container = document.getElementById('hide-' + targetId);
- if (container) {
- addRemoveClass(container, 'hidden', pressed);
- }
-
- // set checkbox, if appropriate
- var checkbox = el.getAttribute('data-controls-checkbox');
- if (checkbox) {
- document.getElementById(checkbox).checked = !!pressed;
- }
-
- // set focus, if appropriate
- var focus = el.getAttribute('data-focus-target');
- if (focus) {
- var focusEl = document.getElementById(focus);
- focusEl.focus();
- setTimeout(function(){ focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0);
- }
-}
-
-function interact(e) {
- e.preventDefault();
- ajaxPost(e.target);
- var identifier = e.target.getAttribute('data-id');
- Array.from(document.getElementsByClassName(identifier))
- .forEach(t => addRemoveClass(t, 'hidden', t.className.indexOf('hidden') == -1));
-}
-
-function toggleMenu(e) {
- var el = e.currentTarget;
- var expanded = el.getAttribute('aria-expanded') == 'false';
- el.setAttribute('aria-expanded', expanded);
- var targetId = el.getAttribute('data-controls');
- if (targetId) {
- var target = document.getElementById(targetId);
- addRemoveClass(target, 'is-active', expanded);
- }
-}
-
-function ajaxPost(form) {
- fetch(form.action, {
- method : "POST",
- body: new FormData(form)
- });
-}
-
-function addRemoveClass(el, classname, bool) {
- if (bool) {
- addClass(el, classname);
- } else {
- removeClass(el, classname);
- }
-}
-
-function addClass(el, classname) {
- var classes = el.className.split(' ');
- if (classes.indexOf(classname) > -1) {
- return;
- }
- el.className = classes.concat(classname).join(' ');
-}
-
-function removeClass(el, className) {
- var classes = [];
- if (el.className) {
- classes = el.className.split(' ');
- }
- const idx = classes.indexOf(className);
- if (idx > -1) {
- classes.splice(idx, 1);
- }
- el.className = classes.join(' ');
-}
diff --git a/bookwyrm/static/js/tabs.js b/bookwyrm/static/js/vendor/tabs.js
similarity index 100%
rename from bookwyrm/static/js/tabs.js
rename to bookwyrm/static/js/vendor/tabs.js
diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html
index 4d978d7e..d263e0e0 100644
--- a/bookwyrm/templates/book/book.html
+++ b/bookwyrm/templates/book/book.html
@@ -129,7 +129,7 @@
{% trans 'Add Description' as button_text %}
{% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add-description" controls_uid=book.id focus="id_description" hide_active=True id="hide-description" %}
-