Merge branch 'book-file-links' into autocomplete

This commit is contained in:
Mouse Reeve
2022-01-10 15:32:20 -08:00
301 changed files with 25881 additions and 7336 deletions

View File

@ -8,6 +8,44 @@ body {
flex-direction: column;
}
button {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
/* inherit font, color & alignment from ancestor */
color: inherit;
font: inherit;
text-align: inherit;
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
line-height: normal;
/* Corrects font smoothing for webkit */
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
/* Corrects inability to style clickable `input` types in iOS */
-webkit-appearance: none;
/* Generalizes pointer cursor */
cursor: pointer;
}
button::-moz-focus-inner {
/* Remove excess padding and border in Firefox 4+ */
border: 0;
padding: 0;
}
/* Better accessibility for keyboard users */
*:focus-visible {
outline-style: auto !important;
}
.image {
overflow: hidden;
}
@ -29,10 +67,38 @@ body {
overflow-x: auto;
}
.modal-card {
pointer-events: none;
}
.modal-card > * {
pointer-events: all;
}
/* stylelint-disable no-descending-specificity */
.modal-card:focus {
outline-style: auto;
}
.modal-card:focus:not(:focus-visible) {
outline-style: initial;
}
.modal-card:focus-visible {
outline-style: auto;
}
/* stylelint-enable no-descending-specificity */
.modal-card.is-fullwidth {
min-width: 75% !important;
}
@media only screen and (min-width: 769px) {
.modal-card.is-thin {
width: 350px !important;
}
}
.modal-card-body {
max-height: 70vh;
}
@ -69,6 +135,18 @@ body {
border-bottom: 1px solid #ededed;
border-right: 0;
}
.is-flex-direction-row-mobile {
flex-direction: row !important;
}
.is-flex-direction-column-mobile {
flex-direction: column !important;
}
}
.tag.is-small {
height: auto;
}
.button.is-transparent {
@ -93,7 +171,34 @@ body {
display: inline !important;
}
input[type=file]::file-selector-button {
button .button-invisible-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 1rem;
box-sizing: border-box;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
background: rgba(0, 0, 0, 66%);
color: white;
opacity: 0;
transition: opacity 0.2s ease;
}
button:hover .button-invisible-overlay,
button:active .button-invisible-overlay,
button:focus-visible .button-invisible-overlay {
opacity: 1;
}
/** File input styles
******************************************************************************/
input[type="file"]::file-selector-button {
-moz-appearance: none;
-webkit-appearance: none;
background-color: #fff;
@ -114,15 +219,40 @@ input[type=file]::file-selector-button {
white-space: nowrap;
}
input[type=file]::file-selector-button:hover {
input[type="file"]::file-selector-button:hover {
border-color: #b5b5b5;
color: #363636;
}
details .dropdown-menu {
display: block !important;
/** General `details` element styles
******************************************************************************/
details summary {
cursor: pointer;
}
summary::-webkit-details-marker {
display: none;
}
details summary::marker {
content: none;
}
details.detail-pinned-button summary {
position: absolute;
right: 0;
}
details.detail-pinned-button form {
float: left;
width: 100%;
margin-top: 1em;
}
/** Dropdown w/ Details element
******************************************************************************/
details.dropdown[open] summary.dropdown-trigger::before {
content: "";
position: fixed;
@ -132,19 +262,83 @@ details.dropdown[open] summary.dropdown-trigger::before {
right: 0;
}
summary::marker {
content: none;
details.dropdown .dropdown-menu {
display: block !important;
}
.detail-pinned-button summary {
details.dropdown .dropdown-menu button {
/* Fix weird Safari defaults */
box-sizing: border-box;
}
details.dropdown .dropdown-menu button:focus-visible,
details.dropdown .dropdown-menu a:focus-visible {
outline-style: auto;
outline-offset: -2px;
}
@media only screen and (max-width: 768px) {
details.dropdown[open] summary.dropdown-trigger::before {
background-color: rgba(0, 0, 0, 50%);
z-index: 30;
}
details .dropdown-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex !important;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 100;
}
details .dropdown-menu > * {
pointer-events: all;
}
}
/** Details panel
******************************************************************************/
details.details-panel {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 10%);
transition: box-shadow 0.2s ease;
padding: 0.75rem;
}
details[open].details-panel,
details.details-panel:hover {
box-shadow: 0 0 0 1px rgba(0, 0, 0, 20%);
}
details.details-panel summary {
position: relative;
}
details.details-panel summary .details-close {
position: absolute;
right: 0;
top: 0;
transform: rotate(45deg);
transition: transform 0.2s ease;
}
.detail-pinned-button form {
float: left;
width: -webkit-fill-available;
margin-top: 1em;
details[open].details-panel summary .details-close {
transform: rotate(0deg);
}
@media only screen and (min-width: 769px) {
.details-panel .filters-field:not(:last-child) {
border-right: 1px solid rgba(0, 0, 0, 10%);
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
}
/** Shelving
@ -153,7 +347,7 @@ summary::marker {
/** @todo Replace icons with SVG symbols.
@see https://www.youtube.com/watch?v=9xXBYcWgCHA */
.shelf-option:disabled > *::after {
font-family: "icomoon"; /* stylelint-disable font-family-no-missing-generic-family-keyword */
font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
content: "\e919"; /* icon-check */
margin-left: 0.5em;
}
@ -161,14 +355,14 @@ summary::marker {
/** Toggles
******************************************************************************/
.toggle-button[aria-pressed=true],
.toggle-button[aria-pressed=true]:hover {
background-color: hsl(171, 100%, 41%);
.toggle-button[aria-pressed="true"],
.toggle-button[aria-pressed="true"]:hover {
background-color: hsl(171deg, 100%, 41%);
color: white;
}
.hide-active[aria-pressed=true],
.hide-inactive[aria-pressed=false] {
.hide-active[aria-pressed="true"],
.hide-inactive[aria-pressed="false"] {
display: none;
}
@ -225,36 +419,36 @@ summary::marker {
/* All stars are visually filled by default. */
.form-rate-stars .icon::before {
content: '\e9d9'; /* icon-star-full */
content: "\e9d9"; /* icon-star-full */
}
/* Icons directly following half star inputs are marked as half */
.form-rate-stars input.half:checked ~ .icon::before {
content: '\e9d8'; /* icon-star-half */
content: "\e9d8"; /* icon-star-half */
}
/* stylelint-disable no-descending-specificity */
.form-rate-stars input.half:checked + input + .icon:hover::before {
content: '\e9d8' !important; /* icon-star-half */
content: "\e9d8" !important; /* icon-star-half */
}
/* Icons directly following half check inputs that follow the checked input are emptied. */
.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
content: '\e9d7'; /* icon-star-empty */
content: "\e9d7"; /* icon-star-empty */
}
/* Icons directly following inputs that follow the checked input are emptied. */
.form-rate-stars input:checked ~ input + .icon::before {
content: '\e9d7'; /* icon-star-empty */
content: "\e9d7"; /* icon-star-empty */
}
/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
.form-rate-stars:hover .icon.icon::before {
content: '\e9d9' !important; /* icon-star-full */
content: "\e9d9" !important; /* icon-star-full */
}
.form-rate-stars .icon:hover ~ .icon::before {
content: '\e9d7' !important; /* icon-star-empty */
content: "\e9d7" !important; /* icon-star-empty */
}
/** Book covers
@ -322,6 +516,8 @@ summary::marker {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1em;
white-space: initial;
text-align: center;
}
@ -360,7 +556,7 @@ summary::marker {
.quote > blockquote::before,
.quote > blockquote::after {
font-family: 'icomoon';
font-family: icomoon;
position: absolute;
}
@ -531,6 +727,10 @@ ol.ordered-list li::before {
}
}
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
/* Threads
******************************************************************************/
@ -562,6 +762,74 @@ ol.ordered-list li::before {
padding: 0 0.75em;
}
/* Breadcrumbs
******************************************************************************/
.books-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
align-items: end;
justify-items: stretch;
}
.books-grid > .is-big {
grid-column: span 2;
grid-row: span 2;
justify-self: stretch;
}
.books-grid .book-cover {
width: 100%;
}
.books-grid .book-title {
--height-basis: 1.35rem;
display: block;
margin-top: 0.5rem;
line-height: var(--height-basis);
min-height: calc(2 * var(--height-basis));
}
@media only screen and (min-width: 769px) {
.books-grid {
gap: 1.5rem;
grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
}
}
/* Copy
******************************************************************************/
.horizontal-copy {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
}
.horizontal-copy textarea {
min-width: initial;
white-space: nowrap;
}
.horizontal-copy button {
align-self: stretch;
height: unset;
}
.vertical-copy {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.vertical-copy button {
width: 100%;
}
/* Dimensions
* @todo These could be in rem.
******************************************************************************/
@ -944,3 +1212,93 @@ ol.ordered-list li::before {
margin-bottom: 0.75rem !important;
}
}
/* Gaps (for Flexbox and Grid)
*
* Those are supplementary rules to Bulmas. They follow the same conventions.
* Add those youll need.
******************************************************************************/
.is-gap-0 {
gap: 0;
}
.is-gap-1 {
gap: 0.25rem;
}
.is-gap-2 {
gap: 0.5rem;
}
.is-gap-3 {
gap: 0.75rem;
}
.is-gap-4 {
gap: 1rem;
}
.is-gap-5 {
gap: 1.5rem;
}
.is-gap-6 {
gap: 3rem;
}
.is-row-gap-0 {
row-gap: 0;
}
.is-row-gap-1 {
row-gap: 0.25rem;
}
.is-row-gap-2 {
row-gap: 0.5rem;
}
.is-row-gap-3 {
row-gap: 0.75rem;
}
.is-row-gap-4 {
row-gap: 1rem;
}
.is-row-gap-5 {
row-gap: 1.5rem;
}
.is-row-gap-6 {
row-gap: 3rem;
}
.is-column-gap-0 {
column-gap: 0;
}
.is-column-gap-1 {
column-gap: 0.25rem;
}
.is-column-gap-2 {
column-gap: 0.5rem;
}
.is-column-gap-3 {
column-gap: 0.75rem;
}
.is-column-gap-4 {
column-gap: 1rem;
}
.is-column-gap-5 {
column-gap: 1.5rem;
}
.is-column-gap-6 {
column-gap: 3rem;
}

View File

@ -0,0 +1,95 @@
Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name
'Source'. All Rights Reserved. Source is a trademark of Adobe in the United
States and/or other countries. Copyright 2019 Google LLC.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,30 @@
# Font Squirrel Font-face Generator Configuration File
# Upload this file to the generator to recreate the settings
# you used to create these fonts.
{
"mode": "optimal",
"formats":
[
"woff",
"woff2"
],
"tt_instructor": "default",
"fix_gasp": "xy",
"fix_vertical_metrics": "Y",
"metrics_ascent": "",
"metrics_descent": "",
"metrics_linegap": "",
"add_spaces": "Y",
"add_hyphens": "Y",
"fallback": "none",
"fallback_custom": "100",
"options_subset": "basic",
"subset_custom": "",
"subset_custom_range": "",
"subset_ot_features_list": "",
"css_stylesheet": "stylesheet.css",
"filename_suffix": "-webfont",
"emsquare": "2048",
"spacing_adjustment": "0"
}

View File

@ -0,0 +1,19 @@
@font-face {
font-family: 'dm_serif_display';
src: url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff2') format('woff2'),
url('../fonts/dm_serif_display/dmserifdisplay-italic-webfont.woff') format('woff');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'dm_serif_display';
src: url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff2') format('woff2'),
url('../fonts/dm_serif_display/dmserifdisplay-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
.is-serif {
font-family: 'dm_serif_display', Georgia, serif;
}

View File

@ -25,6 +25,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon.is-small {
font-size: small;
}
.icon-book:before {
content: "\e901";
}

View File

@ -1,21 +0,0 @@
/* exported BlockHref */
let BlockHref = new class {
constructor() {
document.querySelectorAll('[data-href]')
.forEach(t => t.addEventListener('click', this.followLink.bind(this)));
}
/**
* Follow a fake link
*
* @param {Event} event
* @return {undefined}
*/
followLink(event) {
const url = event.currentTarget.dataset.href;
window.location.href = url;
}
}();

View File

@ -1,7 +1,7 @@
/* exported BookWyrm */
/* globals TabGroup */
let BookWyrm = new class {
let BookWyrm = new (class {
constructor() {
this.MAX_FILE_SIZE_BYTES = 10 * 1000000;
this.initOnDOMLoaded();
@ -10,48 +10,43 @@ let BookWyrm = new class {
}
initEventListeners() {
document.querySelectorAll('[data-controls]')
.forEach(button => button.addEventListener(
'click',
this.toggleAction.bind(this))
);
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(".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(".hidden-form input")
.forEach((button) => button.addEventListener("change", this.revealForm.bind(this)));
document.querySelectorAll('[data-hides]')
.forEach(button => button.addEventListener(
'change',
this.hideForm.bind(this))
);
document
.querySelectorAll("[data-hides]")
.forEach((button) => button.addEventListener("change", this.hideForm.bind(this)));
document.querySelectorAll('[data-back]')
.forEach(button => button.addEventListener(
'click',
this.back)
);
document
.querySelectorAll("[data-back]")
.forEach((button) => button.addEventListener("click", this.back));
document.querySelectorAll('input[type="file"]')
.forEach(node => node.addEventListener(
'change',
this.disableIfTooLarge.bind(this)
));
document.querySelectorAll('[data-duplicate]')
.forEach(node => node.addEventListener(
'click',
this.duplicateInput.bind(this)
))
document
.querySelectorAll('input[type="file"]')
.forEach((node) => node.addEventListener("change", this.disableIfTooLarge.bind(this)));
document
.querySelectorAll("[data-modal-open]")
.forEach((node) => node.addEventListener("click", this.handleModalButton.bind(this)));
document
.querySelectorAll("[data-duplicate]")
.forEach((node) => node.addEventListener("click", this.duplicateInput.bind(this)));
document
.querySelectorAll("details.dropdown")
.forEach((node) =>
node.addEventListener("toggle", this.handleDetailsDropdown.bind(this))
);
}
/**
@ -60,15 +55,15 @@ let BookWyrm = new class {
initOnDOMLoaded() {
const bookwyrm = this;
window.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-group')
.forEach(tabs => new TabGroup(tabs));
document.querySelectorAll('input[type="file"]').forEach(
bookwyrm.disableIfTooLarge.bind(bookwyrm)
);
document.querySelectorAll('[data-copytext]').forEach(
bookwyrm.copyText.bind(bookwyrm)
);
window.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".tab-group").forEach((tabs) => new TabGroup(tabs));
document
.querySelectorAll('input[type="file"]')
.forEach(bookwyrm.disableIfTooLarge.bind(bookwyrm));
document.querySelectorAll("[data-copytext]").forEach(bookwyrm.copyText.bind(bookwyrm));
document
.querySelectorAll(".modal.is-active")
.forEach(bookwyrm.handleActiveModal.bind(bookwyrm));
});
}
@ -77,8 +72,7 @@ let BookWyrm = new class {
*/
initReccuringTasks() {
// Polling
document.querySelectorAll('[data-poll]')
.forEach(liveArea => this.polling(liveArea));
document.querySelectorAll("[data-poll]").forEach((liveArea) => this.polling(liveArea));
}
/**
@ -104,15 +98,19 @@ let BookWyrm = new class {
const bookwyrm = this;
delay = delay || 10000;
delay += (Math.random() * 1000);
delay += Math.random() * 1000;
setTimeout(function() {
fetch('/api/updates/' + counter.dataset.poll)
.then(response => response.json())
.then(data => bookwyrm.updateCountElement(counter, data));
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);
bookwyrm.polling(counter, delay * 1.25);
},
delay,
counter
);
}
/**
@ -127,60 +125,56 @@ let BookWyrm = new class {
const count_by_type = data.count_by_type;
const currentCount = counter.innerText;
const hasMentions = data.has_mentions;
const allowedStatusTypesEl = document.getElementById('unread-notifications-wrapper');
const allowedStatusTypesEl = document.getElementById("unread-notifications-wrapper");
// If we're on the right counter element
if (counter.closest('[data-poll-wrapper]').contains(allowedStatusTypesEl)) {
if (counter.closest("[data-poll-wrapper]").contains(allowedStatusTypesEl)) {
const allowedStatusTypes = JSON.parse(allowedStatusTypesEl.textContent);
// For keys in common between allowedStatusTypes and count_by_type
// This concerns 'review', 'quotation', 'comment'
count = allowedStatusTypes.reduce(function(prev, currentKey) {
count = allowedStatusTypes.reduce(function (prev, currentKey) {
const currentValue = count_by_type[currentKey] | 0;
return prev + currentValue;
}, 0);
// Add all the "other" in count_by_type if 'everything' is allowed
if (allowedStatusTypes.includes('everything')) {
if (allowedStatusTypes.includes("everything")) {
// Clone count_by_type with 0 for reviews/quotations/comments
const count_by_everything_else = Object.assign(
{},
count_by_type,
{review: 0, quotation: 0, comment: 0}
);
const count_by_everything_else = Object.assign({}, count_by_type, {
review: 0,
quotation: 0,
comment: 0,
});
count = Object.keys(count_by_everything_else).reduce(
function(prev, currentKey) {
const currentValue =
count_by_everything_else[currentKey] | 0
count = Object.keys(count_by_everything_else).reduce(function (prev, currentKey) {
const currentValue = count_by_everything_else[currentKey] | 0;
return prev + currentValue;
},
count
);
return prev + currentValue;
}, count);
}
}
if (count != currentCount) {
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-hidden', count < 1);
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-hidden", count < 1);
counter.innerText = count;
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'is-danger', hasMentions);
this.addRemoveClass(counter.closest("[data-poll-wrapper]"), "is-danger", hasMentions);
}
}
/**
* Show form.
*
*
* @param {Event} event
* @return {undefined}
*/
revealForm(event) {
let trigger = event.currentTarget;
let hidden = trigger.closest('.hidden-form').querySelectorAll('.is-hidden')[0];
let hidden = trigger.closest(".hidden-form").querySelectorAll(".is-hidden")[0];
if (hidden) {
this.addRemoveClass(hidden, 'is-hidden', !hidden);
this.addRemoveClass(hidden, "is-hidden", !hidden);
}
}
@ -192,10 +186,10 @@ let BookWyrm = new class {
*/
hideForm(event) {
let trigger = event.currentTarget;
let targetId = trigger.dataset.hides
let visible = document.getElementById(targetId)
let targetId = trigger.dataset.hides;
let visible = document.getElementById(targetId);
this.addRemoveClass(visible, 'is-hidden', true);
this.addRemoveClass(visible, "is-hidden", true);
}
/**
@ -210,31 +204,34 @@ let BookWyrm = new class {
if (!trigger.dataset.allowDefault || event.currentTarget == event.target) {
event.preventDefault();
}
let pressed = trigger.getAttribute('aria-pressed') === 'false';
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'
));
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')) {
if (targetId && !trigger.classList.contains("pulldown-menu")) {
let target = document.getElementById(targetId);
this.addRemoveClass(target, 'is-hidden', !pressed);
this.addRemoveClass(target, 'is-active', pressed);
this.addRemoveClass(target, "is-hidden", !pressed);
this.addRemoveClass(target, "is-active", pressed);
}
// Show/hide pulldown-menus.
if (trigger.classList.contains('pulldown-menu')) {
if (trigger.classList.contains("pulldown-menu")) {
this.toggleMenu(trigger, targetId);
}
// Show/hide container.
let container = document.getElementById('hide_' + targetId);
let container = document.getElementById("hide_" + targetId);
if (container) {
this.toggleContainer(container, pressed);
@ -271,14 +268,14 @@ let BookWyrm = new class {
* @return {undefined}
*/
toggleMenu(trigger, targetId) {
let expanded = trigger.getAttribute('aria-expanded') == 'false';
let expanded = trigger.getAttribute("aria-expanded") == "false";
trigger.setAttribute('aria-expanded', expanded);
trigger.setAttribute("aria-expanded", expanded);
if (targetId) {
let target = document.getElementById(targetId);
this.addRemoveClass(target, 'is-active', expanded);
this.addRemoveClass(target, "is-active", expanded);
}
}
@ -290,7 +287,7 @@ let BookWyrm = new class {
* @return {undefined}
*/
toggleContainer(container, pressed) {
this.addRemoveClass(container, 'is-hidden', pressed);
this.addRemoveClass(container, "is-hidden", pressed);
}
/**
@ -327,7 +324,7 @@ let BookWyrm = new class {
node.focus();
setTimeout(function() {
setTimeout(function () {
node.selectionStart = node.selectionEnd = 10000;
}, 0);
}
@ -347,15 +344,17 @@ let BookWyrm = new class {
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
));
relatedforms.forEach((relatedForm) =>
bookwyrm.addRemoveClass(
relatedForm,
"is-hidden",
relatedForm.className.indexOf("is-hidden") == -1
)
);
this.ajaxPost(form).catch(error => {
this.ajaxPost(form).catch((error) => {
// @todo Display a notification in the UI instead.
console.warn('Request failed:', error);
console.warn("Request failed:", error);
});
}
@ -367,11 +366,11 @@ let BookWyrm = new class {
*/
ajaxPost(form) {
return fetch(form.action, {
method : "POST",
method: "POST",
body: new FormData(form),
headers: {
'Accept': 'application/json',
}
Accept: "application/json",
},
});
}
@ -396,21 +395,112 @@ let BookWyrm = new class {
const element = eventOrElement.currentTarget || eventOrElement;
const submits = element.form.querySelectorAll('[type="submit"]');
const warns = element.parentElement.querySelectorAll('.file-too-big');
const isTooBig = element.files &&
element.files[0] &&
element.files[0].size > MAX_FILE_SIZE_BYTES;
const warns = element.parentElement.querySelectorAll(".file-too-big");
const isTooBig =
element.files && element.files[0] && element.files[0].size > MAX_FILE_SIZE_BYTES;
if (isTooBig) {
submits.forEach(submitter => submitter.disabled = true);
warns.forEach(
sib => addRemoveClass(sib, 'is-hidden', false)
);
submits.forEach((submitter) => (submitter.disabled = true));
warns.forEach((sib) => addRemoveClass(sib, "is-hidden", false));
} else {
submits.forEach(submitter => submitter.disabled = false);
warns.forEach(
sib => addRemoveClass(sib, 'is-hidden', true)
);
submits.forEach((submitter) => (submitter.disabled = false));
warns.forEach((sib) => addRemoveClass(sib, "is-hidden", true));
}
}
/**
* Handle the modal component with a button trigger.
*
* @param {Event} event - Event fired by an element
* with the `data-modal-open` attribute
* pointing to a modal by its id.
* @return {undefined}
*
* See https://github.com/bookwyrm-social/bookwyrm/pull/1633
* for information about using the modal.
*/
handleModalButton(event) {
const { handleFocusTrap } = this;
const modalButton = event.currentTarget;
const targetModalId = modalButton.dataset.modalOpen;
const htmlElement = document.querySelector("html");
const modal = document.getElementById(targetModalId);
if (!modal) {
return;
}
// Helper functions
function handleModalOpen(modalElement) {
event.preventDefault();
htmlElement.classList.add("is-clipped");
modalElement.classList.add("is-active");
modalElement.getElementsByClassName("modal-card")[0].focus();
const closeButtons = modalElement.querySelectorAll("[data-modal-close]");
closeButtons.forEach((button) => {
button.addEventListener("click", function () {
handleModalClose(modalElement);
});
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
handleModalClose(modalElement);
}
});
modalElement.addEventListener("keydown", handleFocusTrap);
}
function handleModalClose(modalElement) {
modalElement.removeEventListener("keydown", handleFocusTrap);
htmlElement.classList.remove("is-clipped");
modalElement.classList.remove("is-active");
modalButton.focus();
}
// Open modal
handleModalOpen(modal);
}
/**
* Handle the modal component when opened at page load.
*
* @param {Element} modalElement - Active modal element
* @return {undefined}
*
*/
handleActiveModal(modalElement) {
if (!modalElement) {
return;
}
const { handleFocusTrap } = this;
modalElement.getElementsByClassName("modal-card")[0].focus();
const closeButtons = modalElement.querySelectorAll("[data-modal-close]");
closeButtons.forEach((button) => {
button.addEventListener("click", function () {
handleModalClose(modalElement);
});
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
handleModalClose(modalElement);
}
});
modalElement.addEventListener("keydown", handleFocusTrap);
function handleModalClose(modalElement) {
modalElement.removeEventListener("keydown", handleFocusTrap);
history.back();
}
}
@ -422,31 +512,27 @@ let BookWyrm = new class {
* @return {undefined}
*/
displayPopUp(url, windowName) {
window.open(
url,
windowName,
"left=100,top=100,width=430,height=600"
);
window.open(url, windowName, "left=100,top=100,width=430,height=600");
}
duplicateInput (event ) {
duplicateInput(event) {
const trigger = event.currentTarget;
const input_id = trigger.dataset['duplicate']
const input_id = trigger.dataset["duplicate"];
const orig = document.getElementById(input_id);
const parent = orig.parentNode;
const new_count = parent.querySelectorAll("input").length + 1
const new_count = parent.querySelectorAll("input").length + 1;
let input = orig.cloneNode();
input.id += ("-" + (new_count))
input.value = ""
input.id += "-" + new_count;
input.value = "";
let label = parent.querySelector("label").cloneNode();
label.setAttribute("for", input.id)
label.setAttribute("for", input.id);
parent.appendChild(label)
parent.appendChild(input)
parent.appendChild(label);
parent.appendChild(input);
}
/**
@ -461,25 +547,115 @@ let BookWyrm = new class {
copyText(textareaEl) {
const text = textareaEl.textContent;
const copyButtonEl = document.createElement('button');
const copyButtonEl = document.createElement("button");
copyButtonEl.textContent = textareaEl.dataset.copytextLabel;
copyButtonEl.classList.add(
"mt-2",
"button",
"is-small",
"is-fullwidth",
"is-primary",
"is-light"
);
copyButtonEl.addEventListener('click', () => {
navigator.clipboard.writeText(text).then(function() {
textareaEl.classList.add('is-success');
copyButtonEl.classList.replace('is-primary', 'is-success');
copyButtonEl.classList.add("button", "is-small", "is-primary", "is-light");
copyButtonEl.addEventListener("click", () => {
navigator.clipboard.writeText(text).then(function () {
textareaEl.classList.add("is-success");
copyButtonEl.classList.replace("is-primary", "is-success");
copyButtonEl.textContent = textareaEl.dataset.copytextSuccess;
});
});
textareaEl.parentNode.appendChild(copyButtonEl)
textareaEl.parentNode.appendChild(copyButtonEl);
}
}();
/**
* Handle the details dropdown component.
*
* @param {Event} event - Event fired by a `details` element
* with the `dropdown` class name, on toggle.
* @return {undefined}
*/
handleDetailsDropdown(event) {
const detailsElement = event.target;
const summaryElement = detailsElement.querySelector("summary");
const menuElement = detailsElement.querySelector(".dropdown-menu");
const htmlElement = document.querySelector("html");
if (detailsElement.open) {
// Focus first menu element
menuElement
.querySelectorAll("a[href]:not([disabled]), button:not([disabled])")[0]
.focus();
// Enable focus trap
menuElement.addEventListener("keydown", this.handleFocusTrap);
// Close on Esc
detailsElement.addEventListener("keydown", handleEscKey);
// Clip page if Mobile
if (this.isMobile()) {
htmlElement.classList.add("is-clipped");
}
} else {
summaryElement.focus();
// Disable focus trap
menuElement.removeEventListener("keydown", this.handleFocusTrap);
// Unclip page
if (this.isMobile()) {
htmlElement.classList.remove("is-clipped");
}
}
function handleEscKey(event) {
if (event.key !== "Escape") {
return;
}
summaryElement.click();
}
}
/**
* Check if windows matches mobile media query.
*
* @return {Boolean}
*/
isMobile() {
return window.matchMedia("(max-width: 768px)").matches;
}
/**
* Focus trap handler
*
* @param {Event} event - Keydown event.
* @return {undefined}
*/
handleFocusTrap(event) {
if (event.key !== "Tab") {
return;
}
const focusableEls = event.currentTarget.querySelectorAll(
[
"a[href]:not([disabled])",
"button:not([disabled])",
"textarea:not([disabled])",
'input:not([type="hidden"]):not([disabled])',
"select:not([disabled])",
"details:not([disabled])",
'[tabindex]:not([tabindex="-1"]):not([disabled])',
].join(",")
);
const firstFocusableEl = focusableEls[0];
const lastFocusableEl = focusableEls[focusableEls.length - 1];
if (event.shiftKey) {
/* Shift + tab */ if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
event.preventDefault();
}
} /* Tab */ else {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
event.preventDefault();
}
}
}
})();

View File

@ -1,34 +0,0 @@
(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
* <input
* type="checkbox"
* data-action="toggle-all"
* data-target="failed-imports"
* >
* @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('[data-action="toggle-all"]')
.forEach(input => {
input.addEventListener('change', toggleAllCheckboxes);
});
})();

View File

@ -1,13 +1,13 @@
/* exported LocalStorageTools */
/* globals BookWyrm */
let LocalStorageTools = new class {
let LocalStorageTools = new (class {
constructor() {
document.querySelectorAll('[data-hide]')
.forEach(t => this.setDisplay(t));
document.querySelectorAll("[data-hide]").forEach((t) => this.setDisplay(t));
document.querySelectorAll('.set-display')
.forEach(t => t.addEventListener('click', this.updateDisplay.bind(this)));
document
.querySelectorAll(".set-display")
.forEach((t) => t.addEventListener("click", this.updateDisplay.bind(this)));
}
/**
@ -23,8 +23,9 @@ let LocalStorageTools = new class {
window.localStorage.setItem(key, value);
document.querySelectorAll('[data-hide="' + key + '"]')
.forEach(node => this.setDisplay(node));
document
.querySelectorAll('[data-hide="' + key + '"]')
.forEach((node) => this.setDisplay(node));
}
/**
@ -38,6 +39,6 @@ let LocalStorageTools = new class {
let key = node.dataset.hide;
let value = window.localStorage.getItem(key);
BookWyrm.addRemoveClass(node, 'is-hidden', value);
BookWyrm.addRemoveClass(node, "is-hidden", value);
}
}();
})();

View File

@ -1,22 +1,21 @@
/* exported StatusCache */
/* globals BookWyrm */
let StatusCache = new class {
let StatusCache = new (class {
constructor() {
document.querySelectorAll('[data-cache-draft]')
.forEach(t => t.addEventListener('change', this.updateDraft.bind(this)));
document
.querySelectorAll("[data-cache-draft]")
.forEach((t) => t.addEventListener("change", this.updateDraft.bind(this)));
document.querySelectorAll('[data-cache-draft]')
.forEach(t => this.populateDraft(t));
document.querySelectorAll("[data-cache-draft]").forEach((t) => this.populateDraft(t));
document.querySelectorAll('.submit-status')
.forEach(button => button.addEventListener(
'submit',
this.submitStatus.bind(this))
);
document
.querySelectorAll(".submit-status")
.forEach((button) => button.addEventListener("submit", this.submitStatus.bind(this)));
document.querySelectorAll('.form-rate-stars label.icon')
.forEach(button => button.addEventListener('click', this.toggleStar.bind(this)));
document
.querySelectorAll(".form-rate-stars label.icon")
.forEach((button) => button.addEventListener("click", this.toggleStar.bind(this)));
}
/**
@ -80,25 +79,26 @@ let StatusCache = new class {
event.preventDefault();
BookWyrm.addRemoveClass(form, 'is-processing', true);
trigger.setAttribute('disabled', null);
BookWyrm.addRemoveClass(form, "is-processing", true);
trigger.setAttribute("disabled", null);
BookWyrm.ajaxPost(form).finally(() => {
// Change icon to remove ongoing activity on the current UI.
// Enable back the element used to submit the form.
BookWyrm.addRemoveClass(form, 'is-processing', false);
trigger.removeAttribute('disabled');
})
.then(response => {
if (!response.ok) {
throw new Error();
}
this.submitStatusSuccess(form);
})
.catch(error => {
console.warn(error);
this.announceMessage('status-error-message');
});
BookWyrm.ajaxPost(form)
.finally(() => {
// Change icon to remove ongoing activity on the current UI.
// Enable back the element used to submit the form.
BookWyrm.addRemoveClass(form, "is-processing", false);
trigger.removeAttribute("disabled");
})
.then((response) => {
if (!response.ok) {
throw new Error();
}
this.submitStatusSuccess(form);
})
.catch((error) => {
console.warn(error);
this.announceMessage("status-error-message");
});
}
/**
@ -112,12 +112,16 @@ let StatusCache = new class {
let copy = element.cloneNode(true);
copy.id = null;
element.insertAdjacentElement('beforebegin', copy);
element.insertAdjacentElement("beforebegin", copy);
BookWyrm.addRemoveClass(copy, 'is-hidden', false);
setTimeout(function() {
copy.remove();
}, 10000, copy);
BookWyrm.addRemoveClass(copy, "is-hidden", false);
setTimeout(
function () {
copy.remove();
},
10000,
copy
);
}
/**
@ -131,8 +135,9 @@ let StatusCache = new class {
form.reset();
// Clear localstorage
form.querySelectorAll('[data-cache-draft]')
.forEach(node => window.localStorage.removeItem(node.dataset.cacheDraft));
form.querySelectorAll("[data-cache-draft]").forEach((node) =>
window.localStorage.removeItem(node.dataset.cacheDraft)
);
// Close modals
let modal = form.closest(".modal.is-active");
@ -142,8 +147,11 @@ let StatusCache = new class {
// Update shelve buttons
if (form.reading_status) {
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
document
.querySelectorAll("[data-shelve-button-book='" + form.book.value + "']")
.forEach((button) =>
this.cycleShelveButtons(button, form.reading_status.value)
);
}
return;
@ -156,7 +164,7 @@ let StatusCache = new class {
document.querySelector("[data-controls=" + reply.id + "]").click();
}
this.announceMessage('status-success-message');
this.announceMessage("status-success-message");
}
/**
@ -172,8 +180,9 @@ let StatusCache = new class {
let next_identifier = shelf.dataset.shelfNext;
// Set all buttons to hidden
button.querySelectorAll("[data-shelf-identifier]")
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
button
.querySelectorAll("[data-shelf-identifier]")
.forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", true));
// Button that should be visible now
let next = button.querySelector("[data-shelf-identifier=" + next_identifier + "]");
@ -183,15 +192,17 @@ let StatusCache = new class {
// ------ update the dropdown buttons
// Remove existing hidden class
button.querySelectorAll("[data-shelf-dropdown-identifier]")
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", false));
button
.querySelectorAll("[data-shelf-dropdown-identifier]")
.forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", false));
// Remove existing disabled states
button.querySelectorAll("[data-shelf-dropdown-identifier] button")
.forEach(item => item.disabled = false);
button
.querySelectorAll("[data-shelf-dropdown-identifier] button")
.forEach((item) => (item.disabled = false));
next_identifier = next_identifier == 'complete' ? 'read' : next_identifier;
next_identifier = next_identifier == "complete" ? "read" : next_identifier;
// Disable the current state
button.querySelector(
@ -206,8 +217,9 @@ let StatusCache = new class {
BookWyrm.addRemoveClass(main_button, "is-hidden", true);
// Just hide the other two menu options, idk what to do with them
button.querySelectorAll("[data-extra-options]")
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", true));
button
.querySelectorAll("[data-extra-options]")
.forEach((item) => BookWyrm.addRemoveClass(item, "is-hidden", true));
// Close menu
let menu = button.querySelector("details[open]");
@ -235,5 +247,4 @@ let StatusCache = new class {
halfStar.checked = "checked";
}
}
}();
})();