bookish-press 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/components/Auth.svelte +52 -0
- package/components/Auth.svelte.d.ts +16 -0
- package/components/Authentication.d.ts +8 -0
- package/components/Authentication.js +1 -0
- package/components/BookPreview.svelte +87 -0
- package/components/BookPreview.svelte.d.ts +18 -0
- package/components/External.svelte +4 -0
- package/components/External.svelte.d.ts +18 -0
- package/components/Link.svelte +5 -0
- package/components/Link.svelte.d.ts +19 -0
- package/components/chapter/Atom.svelte +36 -0
- package/components/chapter/Atom.svelte.d.ts +19 -0
- package/components/chapter/Block.svelte +32 -0
- package/components/chapter/Block.svelte.d.ts +17 -0
- package/components/chapter/Callout.svelte +10 -0
- package/components/chapter/Callout.svelte.d.ts +17 -0
- package/components/chapter/CaptionedCode.svelte +34 -0
- package/components/chapter/CaptionedCode.svelte.d.ts +17 -0
- package/components/chapter/ChapterBody.svelte +23 -0
- package/components/chapter/ChapterBody.svelte.d.ts +18 -0
- package/components/chapter/Citations.svelte +66 -0
- package/components/chapter/Citations.svelte.d.ts +17 -0
- package/components/chapter/Code.svelte +35 -0
- package/components/chapter/Code.svelte.d.ts +22 -0
- package/components/chapter/Comment.svelte +46 -0
- package/components/chapter/Comment.svelte.d.ts +17 -0
- package/components/chapter/Definition.svelte +38 -0
- package/components/chapter/Definition.svelte.d.ts +20 -0
- package/components/chapter/Embed.svelte +122 -0
- package/components/chapter/Embed.svelte.d.ts +18 -0
- package/components/chapter/ErrorMessage.svelte +4 -0
- package/components/chapter/ErrorMessage.svelte.d.ts +17 -0
- package/components/chapter/Footnote.svelte +37 -0
- package/components/chapter/Footnote.svelte.d.ts +19 -0
- package/components/chapter/Format.svelte +57 -0
- package/components/chapter/Format.svelte.d.ts +18 -0
- package/components/chapter/InlineCode.svelte +12 -0
- package/components/chapter/InlineCode.svelte.d.ts +17 -0
- package/components/chapter/Label.svelte +20 -0
- package/components/chapter/Label.svelte.d.ts +17 -0
- package/components/chapter/Link.svelte +51 -0
- package/components/chapter/Link.svelte.d.ts +17 -0
- package/components/chapter/List.svelte +14 -0
- package/components/chapter/List.svelte.d.ts +17 -0
- package/components/chapter/Marginal.svelte +48 -0
- package/components/chapter/Marginal.svelte.d.ts +19 -0
- package/components/chapter/Paragraph.svelte +15 -0
- package/components/chapter/Paragraph.svelte.d.ts +18 -0
- package/components/chapter/Python.svelte +81 -0
- package/components/chapter/Python.svelte.d.ts +18 -0
- package/components/chapter/Quote.svelte +16 -0
- package/components/chapter/Quote.svelte.d.ts +17 -0
- package/components/chapter/Rule.svelte +4 -0
- package/components/chapter/Rule.svelte.d.ts +17 -0
- package/components/chapter/Table.svelte +29 -0
- package/components/chapter/Table.svelte.d.ts +17 -0
- package/components/chapter/Text.svelte +62 -0
- package/components/chapter/Text.svelte.d.ts +17 -0
- package/components/chapter/renderPosition.d.ts +3 -0
- package/components/chapter/renderPosition.js +4 -0
- package/components/editor/BookishEditor.svelte +755 -0
- package/components/editor/BookishEditor.svelte.d.ts +22 -0
- package/components/editor/CalloutEditor.svelte +10 -0
- package/components/editor/CalloutEditor.svelte.d.ts +17 -0
- package/components/editor/CaptionedCodeEditor.svelte +22 -0
- package/components/editor/CaptionedCodeEditor.svelte.d.ts +17 -0
- package/components/editor/CaretContext.d.ts +18 -0
- package/components/editor/CaretContext.js +1 -0
- package/components/editor/CaretState.d.ts +47 -0
- package/components/editor/CaretState.js +1 -0
- package/components/editor/CaretUtilities.d.ts +5 -0
- package/components/editor/CaretUtilities.js +1 -0
- package/components/editor/CitationsEditor.svelte +42 -0
- package/components/editor/CitationsEditor.svelte.d.ts +17 -0
- package/components/editor/Clipboard.d.ts +3 -0
- package/components/editor/Clipboard.js +1 -0
- package/components/editor/Command.d.ts +18 -0
- package/components/editor/Command.js +1 -0
- package/components/editor/Commands.d.ts +3 -0
- package/components/editor/Commands.js +951 -0
- package/components/editor/ConfirmButton.svelte +43 -0
- package/components/editor/ConfirmButton.svelte.d.ts +18 -0
- package/components/editor/DefinitionEditor.svelte +28 -0
- package/components/editor/DefinitionEditor.svelte.d.ts +17 -0
- package/components/editor/EmbedEditor.svelte +87 -0
- package/components/editor/EmbedEditor.svelte.d.ts +17 -0
- package/components/editor/ImageChooser.svelte +70 -0
- package/components/editor/ImageChooser.svelte.d.ts +18 -0
- package/components/editor/InlineCodeEditor.svelte +9 -0
- package/components/editor/InlineCodeEditor.svelte.d.ts +17 -0
- package/components/editor/LabelEditor.svelte +23 -0
- package/components/editor/LabelEditor.svelte.d.ts +17 -0
- package/components/editor/LanguageEditor.svelte +11 -0
- package/components/editor/LanguageEditor.svelte.d.ts +17 -0
- package/components/editor/LinkEditor.svelte +75 -0
- package/components/editor/LinkEditor.svelte.d.ts +17 -0
- package/components/editor/Note.svelte +9 -0
- package/components/editor/Note.svelte.d.ts +23 -0
- package/components/editor/PositionEditor.svelte +14 -0
- package/components/editor/PositionEditor.svelte.d.ts +18 -0
- package/components/editor/QuoteEditor.svelte +5 -0
- package/components/editor/QuoteEditor.svelte.d.ts +17 -0
- package/components/editor/Switch.svelte +56 -0
- package/components/editor/Switch.svelte.d.ts +20 -0
- package/components/editor/TextEditor.svelte +193 -0
- package/components/editor/TextEditor.svelte.d.ts +23 -0
- package/components/editor/Toggle.svelte +38 -0
- package/components/editor/Toggle.svelte.d.ts +19 -0
- package/components/editor/Toolbar.svelte +202 -0
- package/components/editor/Toolbar.svelte.d.ts +20 -0
- package/components/editor/ToolbarGroup.svelte +41 -0
- package/components/editor/ToolbarGroup.svelte.d.ts +19 -0
- package/components/editor/ToolbarIcon.svelte +22 -0
- package/components/editor/ToolbarIcon.svelte.d.ts +16 -0
- package/components/editor/ToolbarSpacer.svelte +1 -0
- package/components/editor/ToolbarSpacer.svelte.d.ts +19 -0
- package/components/editor/URLEditor.svelte +15 -0
- package/components/editor/URLEditor.svelte.d.ts +18 -0
- package/components/editor/UndoState.d.ts +8 -0
- package/components/editor/UndoState.js +1 -0
- package/components/page/Acknowledgements.svelte +29 -0
- package/components/page/Acknowledgements.svelte.d.ts +14 -0
- package/components/page/Alert.svelte +11 -0
- package/components/page/Alert.svelte.d.ts +23 -0
- package/components/page/Authors.svelte +62 -0
- package/components/page/Authors.svelte.d.ts +20 -0
- package/components/page/BulkReferenceEditor.svelte +32 -0
- package/components/page/BulkReferenceEditor.svelte.d.ts +14 -0
- package/components/page/Chapter.svelte +301 -0
- package/components/page/Chapter.svelte.d.ts +18 -0
- package/components/page/ChapterContext.d.ts +10 -0
- package/components/page/ChapterContext.js +1 -0
- package/components/page/Contexts.d.ts +27 -0
- package/components/page/Contexts.js +21 -0
- package/components/page/DefinitionView.svelte +129 -0
- package/components/page/DefinitionView.svelte.d.ts +18 -0
- package/components/page/Description.svelte +22 -0
- package/components/page/Description.svelte.d.ts +14 -0
- package/components/page/Edition.svelte +106 -0
- package/components/page/Edition.svelte.d.ts +21 -0
- package/components/page/Glossary.svelte +61 -0
- package/components/page/Glossary.svelte.d.ts +14 -0
- package/components/page/Header.svelte +105 -0
- package/components/page/Header.svelte.d.ts +27 -0
- package/components/page/Index.svelte +117 -0
- package/components/page/Index.svelte.d.ts +14 -0
- package/components/page/Instructions.svelte +30 -0
- package/components/page/Instructions.svelte.d.ts +16 -0
- package/components/page/License.svelte +29 -0
- package/components/page/License.svelte.d.ts +14 -0
- package/components/page/Loading.svelte +13 -0
- package/components/page/Loading.svelte.d.ts +19 -0
- package/components/page/Media.svelte +76 -0
- package/components/page/Media.svelte.d.ts +14 -0
- package/components/page/MediaPreview.svelte +11 -0
- package/components/page/MediaPreview.svelte.d.ts +19 -0
- package/components/page/Outline.svelte +173 -0
- package/components/page/Outline.svelte.d.ts +19 -0
- package/components/page/Page.svelte +53 -0
- package/components/page/Page.svelte.d.ts +19 -0
- package/components/page/PossibleChapter.svelte +14 -0
- package/components/page/PossibleChapter.svelte.d.ts +14 -0
- package/components/page/PossibleReference.svelte +16 -0
- package/components/page/PossibleReference.svelte.d.ts +19 -0
- package/components/page/Print.svelte +8 -0
- package/components/page/Print.svelte.d.ts +14 -0
- package/components/page/Reference.svelte +145 -0
- package/components/page/Reference.svelte.d.ts +17 -0
- package/components/page/References.svelte +58 -0
- package/components/page/References.svelte.d.ts +14 -0
- package/components/page/Revisions.svelte +124 -0
- package/components/page/Revisions.svelte.d.ts +14 -0
- package/components/page/Search.svelte +108 -0
- package/components/page/Search.svelte.d.ts +14 -0
- package/components/page/Status.svelte +49 -0
- package/components/page/Status.svelte.d.ts +19 -0
- package/components/page/SubdomainEditor.svelte +30 -0
- package/components/page/SubdomainEditor.svelte.d.ts +14 -0
- package/components/page/TableOfContents.svelte +234 -0
- package/components/page/TableOfContents.svelte.d.ts +14 -0
- package/components/page/TableOfContentsImage.svelte +15 -0
- package/components/page/TableOfContentsImage.svelte.d.ts +16 -0
- package/components/page/TableOfContentsRow.svelte +57 -0
- package/components/page/TableOfContentsRow.svelte.d.ts +24 -0
- package/components/page/ThemeEditor.svelte +52 -0
- package/components/page/ThemeEditor.svelte.d.ts +14 -0
- package/components/page/ThemeEditorPreview.svelte +37 -0
- package/components/page/ThemeEditorPreview.svelte.d.ts +17 -0
- package/components/page/ThemeSetEditor.svelte +17 -0
- package/components/page/ThemeSetEditor.svelte.d.ts +18 -0
- package/components/page/ThemeVariableEditor.svelte +28 -0
- package/components/page/ThemeVariableEditor.svelte.d.ts +18 -0
- package/components/page/Unknown.svelte +29 -0
- package/components/page/Unknown.svelte.d.ts +16 -0
- package/components/page/margins.d.ts +2 -0
- package/components/page/margins.js +132 -0
- package/models/BookLoader.d.ts +2 -0
- package/models/BookLoader.js +67 -0
- package/models/Firebase.d.ts +4 -0
- package/models/Firebase.js +26 -0
- package/models/Firestore.d.ts +22 -0
- package/models/Firestore.js +194 -0
- package/models/book/Book.d.ts +60 -0
- package/models/book/Book.js +135 -0
- package/models/book/BookMedia.d.ts +23 -0
- package/models/book/BookMedia.js +102 -0
- package/models/book/Chapter.d.ts +70 -0
- package/models/book/Chapter.js +221 -0
- package/models/book/ChapterID.d.ts +10 -0
- package/models/book/ChapterID.js +10 -0
- package/models/book/Definition.d.ts +6 -0
- package/models/book/Definition.js +1 -0
- package/models/book/Edition.d.ts +111 -0
- package/models/book/Edition.js +466 -0
- package/models/book/Theme.d.ts +106 -0
- package/models/book/Theme.js +76 -0
- package/models/book/ThemeColors.d.ts +15 -0
- package/models/book/ThemeColors.js +1 -0
- package/models/chapter/AtomNode.d.ts +18 -0
- package/models/chapter/AtomNode.js +31 -0
- package/models/chapter/BlockNode.d.ts +21 -0
- package/models/chapter/BlockNode.js +117 -0
- package/models/chapter/BlocksNode.d.ts +50 -0
- package/models/chapter/BlocksNode.js +709 -0
- package/models/chapter/BlocksNode.test.d.ts +1 -0
- package/models/chapter/BlocksNode.test.js +302 -0
- package/models/chapter/Bookkeeping.d.ts +4 -0
- package/models/chapter/Bookkeeping.js +1 -0
- package/models/chapter/CalloutNode.d.ts +17 -0
- package/models/chapter/CalloutNode.js +36 -0
- package/models/chapter/Caret.d.ts +21 -0
- package/models/chapter/Caret.js +69 -0
- package/models/chapter/ChapterNode.d.ts +34 -0
- package/models/chapter/ChapterNode.js +80 -0
- package/models/chapter/CitationsNode.d.ts +16 -0
- package/models/chapter/CitationsNode.js +17 -0
- package/models/chapter/CodeNode.d.ts +30 -0
- package/models/chapter/CodeNode.js +64 -0
- package/models/chapter/CommentNode.d.ts +18 -0
- package/models/chapter/CommentNode.js +32 -0
- package/models/chapter/DefinitionNode.d.ts +12 -0
- package/models/chapter/DefinitionNode.js +20 -0
- package/models/chapter/Edit.d.ts +7 -0
- package/models/chapter/Edit.js +1 -0
- package/models/chapter/EmbedNode.d.ts +40 -0
- package/models/chapter/EmbedNode.js +89 -0
- package/models/chapter/ErrorNode.d.ts +17 -0
- package/models/chapter/ErrorNode.js +21 -0
- package/models/chapter/FootnoteNode.d.ts +18 -0
- package/models/chapter/FootnoteNode.js +29 -0
- package/models/chapter/FormatNode.d.ts +60 -0
- package/models/chapter/FormatNode.js +614 -0
- package/models/chapter/FormatNode.test.d.ts +1 -0
- package/models/chapter/FormatNode.test.js +54 -0
- package/models/chapter/InlineCodeNode.d.ts +12 -0
- package/models/chapter/InlineCodeNode.js +18 -0
- package/models/chapter/LabelNode.d.ts +17 -0
- package/models/chapter/LabelNode.js +22 -0
- package/models/chapter/LinkNode.d.ts +12 -0
- package/models/chapter/LinkNode.js +16 -0
- package/models/chapter/ListNode.d.ts +51 -0
- package/models/chapter/ListNode.js +370 -0
- package/models/chapter/ListNode.test.d.ts +1 -0
- package/models/chapter/ListNode.test.js +49 -0
- package/models/chapter/MetadataNode.d.ts +17 -0
- package/models/chapter/MetadataNode.js +24 -0
- package/models/chapter/Node.d.ts +36 -0
- package/models/chapter/Node.js +173 -0
- package/models/chapter/Node.test.d.ts +1 -0
- package/models/chapter/Node.test.js +27 -0
- package/models/chapter/ParagraphNode.d.ts +33 -0
- package/models/chapter/ParagraphNode.js +93 -0
- package/models/chapter/ParagraphNode.test.d.ts +1 -0
- package/models/chapter/ParagraphNode.test.js +21 -0
- package/models/chapter/Parser.d.ts +75 -0
- package/models/chapter/Parser.js +824 -0
- package/models/chapter/Parser.test.d.ts +1 -0
- package/models/chapter/Parser.test.js +25 -0
- package/models/chapter/Position.d.ts +2 -0
- package/models/chapter/Position.js +1 -0
- package/models/chapter/QuoteNode.d.ts +25 -0
- package/models/chapter/QuoteNode.js +61 -0
- package/models/chapter/ReferenceNode.d.ts +29 -0
- package/models/chapter/ReferenceNode.js +48 -0
- package/models/chapter/RootNode.d.ts +5 -0
- package/models/chapter/RootNode.js +1 -0
- package/models/chapter/RuleNode.d.ts +15 -0
- package/models/chapter/RuleNode.js +16 -0
- package/models/chapter/TableNode.d.ts +35 -0
- package/models/chapter/TableNode.js +176 -0
- package/models/chapter/TableNode.test.d.ts +1 -0
- package/models/chapter/TableNode.test.js +31 -0
- package/models/chapter/TextNode.d.ts +36 -0
- package/models/chapter/TextNode.js +136 -0
- package/models/chapter/TextNode.test.d.ts +1 -0
- package/models/chapter/TextNode.test.js +6 -0
- package/models/loadEdition.d.ts +5 -0
- package/models/loadEdition.js +16 -0
- package/package.json +189 -0
- package/util/getSubdomain.d.ts +3 -0
- package/util/getSubdomain.js +22 -0
- package/util/isMobile.d.ts +2 -0
- package/util/isMobile.js +6 -0
- package/util/mineReference.d.ts +3 -0
- package/util/mineReference.js +62 -0
- package/util/scrollToEyeLevel.d.ts +2 -0
- package/util/scrollToEyeLevel.js +5 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Hides the outline if there's a left inset marginal that might overlap it.
|
|
2
|
+
// This operates on the DOM directly, as scroll position isn't easily managed in React.
|
|
3
|
+
export function hideOutlineIfObscured() {
|
|
4
|
+
// Get the outline's bounds.
|
|
5
|
+
let outline = document.getElementsByClassName("bookish-outline")[0];
|
|
6
|
+
// If there's no outline, don't bother.
|
|
7
|
+
if (!outline)
|
|
8
|
+
return;
|
|
9
|
+
// Set a threshold for hiding
|
|
10
|
+
let threshold = 100;
|
|
11
|
+
// If the outline isn't affixed to the footer
|
|
12
|
+
if (window.getComputedStyle(outline).getPropertyValue("left") !== "0px") {
|
|
13
|
+
let outlineRect = outline.getBoundingClientRect();
|
|
14
|
+
let overlapRect = undefined;
|
|
15
|
+
// Find the bottom-most left inset marginal within threshold of the outline.
|
|
16
|
+
Array.from(document.getElementsByClassName("bookish-marginal-left-inset")).forEach(el => {
|
|
17
|
+
let insetRect = el.getBoundingClientRect();
|
|
18
|
+
if (!(outlineRect.bottom < insetRect.top - threshold ||
|
|
19
|
+
outlineRect.top > insetRect.bottom + threshold))
|
|
20
|
+
overlapRect = insetRect;
|
|
21
|
+
});
|
|
22
|
+
// If it overlaps, reduce opacity by the proportion of the vertical distance between two rectangles and the height of the marginal.
|
|
23
|
+
if (overlapRect) {
|
|
24
|
+
let overlap = overlapRect;
|
|
25
|
+
let distance = outlineRect.bottom < overlap.top ? threshold :
|
|
26
|
+
outlineRect.bottom < overlap.top + threshold ? threshold - (outlineRect.bottom - overlap.top) :
|
|
27
|
+
outlineRect.top > overlap.bottom - threshold ? threshold - (overlap.bottom - outlineRect.top) :
|
|
28
|
+
outlineRect.top > overlap.bottom ? threshold :
|
|
29
|
+
0;
|
|
30
|
+
let proportion = distance / threshold;
|
|
31
|
+
outline.style.opacity = "" + (proportion * proportion);
|
|
32
|
+
}
|
|
33
|
+
// Otherwise, remove the dimming.
|
|
34
|
+
else
|
|
35
|
+
outline.style.removeProperty("opacity");
|
|
36
|
+
}
|
|
37
|
+
// If it is, it's always visible.
|
|
38
|
+
else
|
|
39
|
+
outline.style.removeProperty("opacity");
|
|
40
|
+
}
|
|
41
|
+
// After each render of the chapter, we need to adjust the layout of marginals, which by default are floating from
|
|
42
|
+
// their little spans within the body of the text.
|
|
43
|
+
//
|
|
44
|
+
// We have to do two things:
|
|
45
|
+
//
|
|
46
|
+
// 1) Lay them out vertically so that they don't overlap.
|
|
47
|
+
// 2) Lay them out horizontally, so they're all left aligned with each other on the right margin of the text.
|
|
48
|
+
//
|
|
49
|
+
// We only want to do this if the marginals are floating; we exclude any marginals that are
|
|
50
|
+
// fixed in a fixed position.
|
|
51
|
+
export function layoutMarginals() {
|
|
52
|
+
// Get the chapter DOM node, so we can calculate margins.
|
|
53
|
+
let chapter = document.getElementsByClassName("bookish-chapter-body")[0];
|
|
54
|
+
// If there's no chapter rendered yet, stop.
|
|
55
|
+
if (chapter === undefined)
|
|
56
|
+
return;
|
|
57
|
+
let book = document.getElementsByClassName("bookish")[0];
|
|
58
|
+
// This is the grid line we'll aline all marginals too horizontally.
|
|
59
|
+
let margin = chapter.getBoundingClientRect().width;
|
|
60
|
+
// This will track the current leading bottom edge for laying out marginals.
|
|
61
|
+
let currentBottom = null;
|
|
62
|
+
// Are there any marginal right inset images that might overlap?
|
|
63
|
+
let rightInsets = document.getElementsByClassName("bookish-marginal-right-inset");
|
|
64
|
+
const marginals = document.getElementsByClassName("bookish-marginal");
|
|
65
|
+
// Layout all of the marginals vertically to prevent overlaps.
|
|
66
|
+
Array.from(marginals).forEach((element) => {
|
|
67
|
+
const el = element;
|
|
68
|
+
if (el === null)
|
|
69
|
+
return;
|
|
70
|
+
if (window.getComputedStyle(el).getPropertyValue("position") !== "fixed") {
|
|
71
|
+
if (el.parentElement) {
|
|
72
|
+
let containingMarginal = el.parentElement.closest(".bookish-marginal");
|
|
73
|
+
// Get the bounds of this marginal so we know it's size.
|
|
74
|
+
let elementBounds = el.getBoundingClientRect();
|
|
75
|
+
// Get the bounds of this marginal's parent so we know it's vertical position.
|
|
76
|
+
let parentBounds = el.parentElement.getBoundingClientRect();
|
|
77
|
+
let parentTop = parentBounds.top + window.scrollY - (book.getBoundingClientRect().top + window.scrollY);
|
|
78
|
+
// If it's inside a marginal, position it below the marginal.
|
|
79
|
+
if (containingMarginal !== null && currentBottom) {
|
|
80
|
+
el.style.left = "0";
|
|
81
|
+
el.style.top = "" + (currentBottom - (containingMarginal.getBoundingClientRect().top + window.scrollY)) + "px";
|
|
82
|
+
currentBottom = currentBottom + elementBounds.height;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// If we don't have a bottom yet, or the current above is above the parent's top,
|
|
86
|
+
// then set the current bottom to the bottom of the parent.
|
|
87
|
+
if (currentBottom === null || currentBottom < parentTop) {
|
|
88
|
+
el.style.top = parentTop + "px";
|
|
89
|
+
currentBottom = parentTop + elementBounds.height;
|
|
90
|
+
}
|
|
91
|
+
// Otherwise, put the marginal below the last marginal's bottom.
|
|
92
|
+
else {
|
|
93
|
+
el.style.top = currentBottom + "px";
|
|
94
|
+
currentBottom = currentBottom + elementBounds.height;
|
|
95
|
+
}
|
|
96
|
+
// If there are any right insets, do any intersect with this marginal?
|
|
97
|
+
// If so, move the marginal below them.
|
|
98
|
+
if (rightInsets.length > 0) {
|
|
99
|
+
Array.from(rightInsets).forEach(inset => {
|
|
100
|
+
// Compute the global positions of the marginal and the potential inset.
|
|
101
|
+
let insetBounds = inset.getBoundingClientRect();
|
|
102
|
+
let insetTop = insetBounds.top + window.scrollY;
|
|
103
|
+
let insetBottom = insetTop + insetBounds.height;
|
|
104
|
+
let elementTop = el.getBoundingClientRect().top + window.scrollY;
|
|
105
|
+
let elementBottom = elementTop + elementBounds.height;
|
|
106
|
+
// If they overlap...
|
|
107
|
+
if (currentBottom !== null) {
|
|
108
|
+
if ((elementTop <= insetTop && elementBottom >= insetTop) ||
|
|
109
|
+
(elementTop <= insetBottom && elementBottom >= insetBottom) ||
|
|
110
|
+
(elementTop >= insetTop && elementBottom <= insetBottom)) {
|
|
111
|
+
// Calculate the new position based on the preferred position, minus the element's height (which was added above), plus the overlap between the inset and the marginal.
|
|
112
|
+
let newTop = currentBottom - elementBounds.height + (insetBottom - elementTop);
|
|
113
|
+
// Reposition the marginal.
|
|
114
|
+
el.style.top = newTop + "px";
|
|
115
|
+
// Update the new bottom.
|
|
116
|
+
currentBottom = newTop + elementBounds.height;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Reposition the marginal horizontally.
|
|
122
|
+
el.style.left = "calc(2rem + " + margin + "px)";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// If it's fixed, remove any explicitly set inline values.
|
|
127
|
+
else {
|
|
128
|
+
el.style.removeProperty("top");
|
|
129
|
+
el.style.removeProperty("left");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import addFormats from 'ajv-formats';
|
|
3
|
+
import Edition from './book/Edition';
|
|
4
|
+
import schema from "../../schemas/book.json";
|
|
5
|
+
// Returns a Promise that, given a URL to
|
|
6
|
+
// 1) a to a valid "book.json" (see /src/schemas/book.json for a validity spec),
|
|
7
|
+
// 2) a "chapters" folder with .md files corresponding to the chapter IDs in the spec,
|
|
8
|
+
// resolves to a Book object with all of the book data.
|
|
9
|
+
// If any errors are encountered, throws an Error.
|
|
10
|
+
export default async function loadBookFromURL(url) {
|
|
11
|
+
let specification = undefined;
|
|
12
|
+
// Fetch the JSON from the given URL
|
|
13
|
+
return fetch(url + "book.json")
|
|
14
|
+
// If there's a valid response, get the JSON
|
|
15
|
+
.then(response => {
|
|
16
|
+
// If it's a valid status, parse the text as JSON.
|
|
17
|
+
if (response.status >= 200 && response.status <= 299)
|
|
18
|
+
return response.json();
|
|
19
|
+
// Otherwise, throw an error with the request failure.
|
|
20
|
+
else
|
|
21
|
+
throw Error(response.statusText);
|
|
22
|
+
})
|
|
23
|
+
// If the JSON doesn't parse...
|
|
24
|
+
.catch(() => {
|
|
25
|
+
throw Error("The book.json file that we received doesn't appear to be a JSON-formatted document.");
|
|
26
|
+
})
|
|
27
|
+
// Validate the specification and then load its chapters...
|
|
28
|
+
.then((book) => {
|
|
29
|
+
// Validate the book schema before we get started.
|
|
30
|
+
const ajv = new Ajv({
|
|
31
|
+
strictTuples: false
|
|
32
|
+
});
|
|
33
|
+
addFormats(ajv);
|
|
34
|
+
// Did the specification have schema errors?
|
|
35
|
+
// Initialize the book as null and set the errors.
|
|
36
|
+
if (!ajv.validate(schema, book)) {
|
|
37
|
+
throw Error("The book specification has validation errors" +
|
|
38
|
+
(ajv.errors ?
|
|
39
|
+
": " + ajv.errors.map(error => url + error.instancePath + " " + error.message).join("\n") :
|
|
40
|
+
"."));
|
|
41
|
+
}
|
|
42
|
+
// Remember the book spec
|
|
43
|
+
specification = book;
|
|
44
|
+
// Map all non-forthcoming chapters to a list of fetch promises
|
|
45
|
+
return Promise.all(specification.chapters.filter((chapter) => !chapter.forthcoming).map((chapter) => fetch(url + "chapters/" + chapter.id + ".md")
|
|
46
|
+
.then((response) => {
|
|
47
|
+
// If we got a reasonable response, process the chapter.
|
|
48
|
+
if (response.ok)
|
|
49
|
+
return response.text();
|
|
50
|
+
else
|
|
51
|
+
throw Error("Unable to load chapter named '" + chapter.id + "'. Make sure the chapter ID and chapter file name match.");
|
|
52
|
+
})
|
|
53
|
+
// Set the text field of the chapter object in the specification
|
|
54
|
+
.then((text) => {
|
|
55
|
+
chapter.text = text;
|
|
56
|
+
})
|
|
57
|
+
// If there's an error, set chapter text to null.
|
|
58
|
+
.catch(() => {
|
|
59
|
+
chapter.text = undefined;
|
|
60
|
+
})));
|
|
61
|
+
})
|
|
62
|
+
// After all the chapter loading promises are done, make the book.
|
|
63
|
+
.then(() => {
|
|
64
|
+
// Construct a Book object given the spec and chapter text
|
|
65
|
+
return new Edition(undefined, undefined, specification);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const app: import("@firebase/app").FirebaseApp | undefined;
|
|
2
|
+
export declare const auth: import("@firebase/auth").Auth | undefined;
|
|
3
|
+
export declare const db: import("@firebase/firestore").Firestore | undefined;
|
|
4
|
+
export declare const storage: import("@firebase/storage").FirebaseStorage | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { initializeApp } from "firebase/app";
|
|
2
|
+
import { connectAuthEmulator, getAuth } from "firebase/auth";
|
|
3
|
+
import { connectFirestoreEmulator, getFirestore } from "firebase/firestore";
|
|
4
|
+
import { connectStorageEmulator, getStorage } from "firebase/storage";
|
|
5
|
+
const isReader = import.meta.env.VITE_READER === "true";
|
|
6
|
+
// Initialize Firebase using the environment variables provided at build time.
|
|
7
|
+
// Only do this if we have environment variables defined. (We won't in the standalone Reader).
|
|
8
|
+
export const app = isReader ? undefined : initializeApp({
|
|
9
|
+
apiKey: import.meta.env.VITE_BOOKISH_FIREBASE_API_KEY,
|
|
10
|
+
authDomain: import.meta.env.VITE_BOOKISH_AUTH_DOMAIN,
|
|
11
|
+
projectId: import.meta.env.VITE_BOOKISH_PROJECT_ID,
|
|
12
|
+
storageBucket: import.meta.env.VITE_BOOKISH_STORAGE_BUCKET,
|
|
13
|
+
messagingSenderId: import.meta.env.VITE_BOOKISH_MESSAGING_SENDER_ID,
|
|
14
|
+
appId: import.meta.env.VITE_BOOKISH_APP_ID
|
|
15
|
+
});
|
|
16
|
+
export const auth = isReader ? undefined : getAuth();
|
|
17
|
+
export const db = isReader ? undefined : getFirestore();
|
|
18
|
+
export const storage = isReader ? undefined : getStorage();
|
|
19
|
+
if (!isReader && import.meta.env.DEV) {
|
|
20
|
+
if (db)
|
|
21
|
+
connectFirestoreEmulator(db, 'localhost', 8080);
|
|
22
|
+
if (auth)
|
|
23
|
+
connectAuthEmulator(auth, "http://localhost:9099");
|
|
24
|
+
if (storage)
|
|
25
|
+
connectStorageEmulator(storage, "localhost", 9199);
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DocumentReference } from "firebase/firestore";
|
|
2
|
+
import Edition from "./book/Edition";
|
|
3
|
+
import Book from "./book/Book";
|
|
4
|
+
import type Chapter from "./book/Chapter";
|
|
5
|
+
import type { ChapterContent } from "./book/Chapter";
|
|
6
|
+
export declare const loadPublishedBooksFromFirestore: () => Promise<Book[]>;
|
|
7
|
+
export declare const loadUsersBooksFromFirestore: (userID: string) => Promise<Book[] | null>;
|
|
8
|
+
export declare const getBookFromIDOrName: (bookIDOrName: string) => Promise<Book | null>;
|
|
9
|
+
export declare const loadBookFromFirestore: (bookID: string) => Promise<Book | null>;
|
|
10
|
+
export declare const loadEditionFromFirestore: (book: Book, editionID: string) => Promise<Edition>;
|
|
11
|
+
export declare const createBookInFirestore: (userID: string) => Promise<string>;
|
|
12
|
+
export declare const addDraftInFirestore: (book: Book) => Promise<void>;
|
|
13
|
+
export declare const publishDraftInFirestore: (book: Book, index: number, published: boolean) => Promise<void>;
|
|
14
|
+
export declare const updateEditionInFirestore: (edition: Edition) => Promise<void>;
|
|
15
|
+
export declare const deleteEditionInFirestore: (book: Book, index: number) => Promise<void>;
|
|
16
|
+
export declare const updateBookInFirestore: (book: Book) => Promise<void>;
|
|
17
|
+
export declare const addChapterInFirestore: (edition: DocumentReference, chapter: ChapterContent) => Promise<DocumentReference>;
|
|
18
|
+
export declare const removeChapterFromEditionInFirestore: (chapter: Chapter) => Promise<void>;
|
|
19
|
+
export declare const loadChapterTextFromFirestore: (chapter: DocumentReference) => Promise<ChapterContent>;
|
|
20
|
+
export declare const updateChapterTextInFirestore: (edition: DocumentReference, chapter: DocumentReference, text: string) => Promise<void>;
|
|
21
|
+
export declare const subdomainIsAvailable: (subdomain: string, book: Book) => Promise<boolean>;
|
|
22
|
+
export declare const getBookIDFromBookName: (subdomain: string) => Promise<string>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { db } from "./Firebase";
|
|
2
|
+
import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc, DocumentReference } from "firebase/firestore";
|
|
3
|
+
import Edition from "./book/Edition";
|
|
4
|
+
import Book from "./book/Book";
|
|
5
|
+
export const loadPublishedBooksFromFirestore = async () => {
|
|
6
|
+
if (!db)
|
|
7
|
+
throw Error("Can't retrieve books, not connected to the database.");
|
|
8
|
+
try {
|
|
9
|
+
const data = await getDocs(collection(db, "books"));
|
|
10
|
+
return data.docs.map(doc => new Book(doc.ref, doc.data())).filter(book => book.hasPublishedEdition());
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw Error("Couldn't load books, problem reading from the database.");
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const loadUsersBooksFromFirestore = async (userID) => {
|
|
17
|
+
if (!db)
|
|
18
|
+
throw Error("Can't retrieve user's books, not connected to Firebase.");
|
|
19
|
+
try {
|
|
20
|
+
const books = await getDocs(query(collection(db, "books"), where("uids", "array-contains", userID)));
|
|
21
|
+
return books.docs.map(doc => new Book(doc.ref, doc.data()));
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error(err);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export const getBookFromIDOrName = async (bookIDOrName) => {
|
|
29
|
+
const book = await loadBookFromFirestore(bookIDOrName);
|
|
30
|
+
if (book)
|
|
31
|
+
return book;
|
|
32
|
+
const bookID = await getBookIDFromBookName(bookIDOrName);
|
|
33
|
+
return loadBookFromFirestore(bookID);
|
|
34
|
+
};
|
|
35
|
+
export const loadBookFromFirestore = async (bookID) => {
|
|
36
|
+
if (!db)
|
|
37
|
+
return null;
|
|
38
|
+
const bookRef = doc(db, "books", bookID);
|
|
39
|
+
const book = await getDoc(bookRef);
|
|
40
|
+
return !book.exists() ? null : new Book(bookRef, book.data());
|
|
41
|
+
};
|
|
42
|
+
export const loadEditionFromFirestore = async (book, editionID) => {
|
|
43
|
+
if (!db)
|
|
44
|
+
throw Error("Can't retrieve edition, not connected to Firebase.");
|
|
45
|
+
const edition = await getDoc(doc(db, "editions", editionID));
|
|
46
|
+
if (edition.exists())
|
|
47
|
+
return new Edition(book, edition.ref, edition.data());
|
|
48
|
+
else
|
|
49
|
+
throw Error("" + editionID + " doesn't exist");
|
|
50
|
+
};
|
|
51
|
+
export const createBookInFirestore = async (userID) => {
|
|
52
|
+
if (!db)
|
|
53
|
+
throw Error("Can't create book, not connected to Firebase.");
|
|
54
|
+
// Make a new empty edition with a single chapter.
|
|
55
|
+
const newEdition = new Edition();
|
|
56
|
+
// Add the current user to the book
|
|
57
|
+
newEdition.addUserID(userID);
|
|
58
|
+
// Add the book to the editions collection
|
|
59
|
+
const editionRef = await addDoc(collection(db, "editions"), newEdition.toObject());
|
|
60
|
+
newEdition.setRef(editionRef);
|
|
61
|
+
// Add a new chapter to the edition and update it in the database.
|
|
62
|
+
await newEdition.addChapter();
|
|
63
|
+
await updateEditionInFirestore(newEdition);
|
|
64
|
+
// Note that we don't create a sub-collection of chapters here.
|
|
65
|
+
// Firestore creates sub-collections automatically when documents are added.
|
|
66
|
+
// Create a new book with the draft first edition and the user ID.
|
|
67
|
+
const newBook = {
|
|
68
|
+
title: "",
|
|
69
|
+
authors: [],
|
|
70
|
+
description: "",
|
|
71
|
+
cover: null,
|
|
72
|
+
revisions: [
|
|
73
|
+
{
|
|
74
|
+
ref: editionRef,
|
|
75
|
+
time: Date.now(),
|
|
76
|
+
summary: "",
|
|
77
|
+
published: false
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
uids: [userID]
|
|
81
|
+
};
|
|
82
|
+
// Create the book
|
|
83
|
+
const bookRef = await addDoc(collection(db, "books"), newBook);
|
|
84
|
+
// Return the book id
|
|
85
|
+
return bookRef.id;
|
|
86
|
+
};
|
|
87
|
+
export const addDraftInFirestore = async (book) => {
|
|
88
|
+
if (!db)
|
|
89
|
+
throw Error("Can't publish draft, no Firebase connection.");
|
|
90
|
+
// Get the latest draft.
|
|
91
|
+
const latestDraft = await book.getDraftEdition();
|
|
92
|
+
if (latestDraft === undefined)
|
|
93
|
+
return;
|
|
94
|
+
// Create a copy of the existing draft as a new edition
|
|
95
|
+
const newDraft = await addDoc(collection(db, "editions"), latestDraft.toObject());
|
|
96
|
+
// Put the new edition in the book.
|
|
97
|
+
const revisions = book.getRevisions();
|
|
98
|
+
revisions.unshift({
|
|
99
|
+
ref: newDraft,
|
|
100
|
+
time: Date.now(),
|
|
101
|
+
summary: "",
|
|
102
|
+
published: false
|
|
103
|
+
});
|
|
104
|
+
book.setRevisions(revisions);
|
|
105
|
+
// Return the revised Book for viewing
|
|
106
|
+
updateBookInFirestore(book);
|
|
107
|
+
};
|
|
108
|
+
export const publishDraftInFirestore = async (book, index, published) => {
|
|
109
|
+
if (!db)
|
|
110
|
+
throw Error("Can't publish draft, no Firebase connection.");
|
|
111
|
+
// Update the edition's published status.
|
|
112
|
+
book.setPublished(published, index);
|
|
113
|
+
// Update the book with the latest published edition's metadata.
|
|
114
|
+
const draft = await book.getLatestPublishedEdition();
|
|
115
|
+
if (draft)
|
|
116
|
+
book.updateMetadataFromEdition(draft);
|
|
117
|
+
// Return the revised Book for viewing
|
|
118
|
+
updateBookInFirestore(book);
|
|
119
|
+
};
|
|
120
|
+
export const updateEditionInFirestore = async (edition) => {
|
|
121
|
+
if (!db)
|
|
122
|
+
throw Error("Can't update edition, not connected to Firebase.");
|
|
123
|
+
// Get the object for the edition so we can store it.
|
|
124
|
+
const spec = edition.toObject();
|
|
125
|
+
const editionRef = edition.getRef();
|
|
126
|
+
// Make sure the given edition has a database reference ID
|
|
127
|
+
if (!editionRef)
|
|
128
|
+
throw Error("Book given has no ID");
|
|
129
|
+
// Update the edition's document.
|
|
130
|
+
await setDoc(doc(db, "editions", editionRef.id), spec);
|
|
131
|
+
// Get the book of the editiion, and if the edition updated is the latest edition,
|
|
132
|
+
// update the book's cache of title, cover, authors, and description.
|
|
133
|
+
if (edition.book !== undefined && edition.isLatestPublishedEdition())
|
|
134
|
+
edition.book.updateMetadataFromEdition(edition);
|
|
135
|
+
};
|
|
136
|
+
export const deleteEditionInFirestore = async (book, index) => {
|
|
137
|
+
if (!db)
|
|
138
|
+
throw Error("Can't update edition, not connected to Firebase.");
|
|
139
|
+
const revisions = book.getRevisions();
|
|
140
|
+
if (index < 0 || index >= revisions.length)
|
|
141
|
+
return;
|
|
142
|
+
await deleteDoc(revisions[index].ref);
|
|
143
|
+
revisions.splice(index, 1);
|
|
144
|
+
book.setRevisions(revisions);
|
|
145
|
+
await updateBookInFirestore(book);
|
|
146
|
+
};
|
|
147
|
+
export const updateBookInFirestore = async (book) => {
|
|
148
|
+
if (!db)
|
|
149
|
+
throw Error("Can't update boo, not connected to Firebase.");
|
|
150
|
+
await setDoc(doc(db, "books", book.getRefID()), book.toObject());
|
|
151
|
+
};
|
|
152
|
+
export const addChapterInFirestore = async (edition, chapter) => {
|
|
153
|
+
if (!db)
|
|
154
|
+
throw Error("Can't add chapter, not connected to Firebase.");
|
|
155
|
+
return await addDoc(collection(db, `editions/${edition.id}/chapters`), chapter);
|
|
156
|
+
};
|
|
157
|
+
export const removeChapterFromEditionInFirestore = async (chapter) => {
|
|
158
|
+
if (!db)
|
|
159
|
+
throw Error("Can't add chapter, not connected to Firebase.");
|
|
160
|
+
const ref = chapter.getRef();
|
|
161
|
+
if (!ref)
|
|
162
|
+
throw Error("Can't delete chapter, it doesn't have a document ID.");
|
|
163
|
+
await deleteDoc(ref);
|
|
164
|
+
};
|
|
165
|
+
export const loadChapterTextFromFirestore = async (chapter) => {
|
|
166
|
+
if (!db)
|
|
167
|
+
throw Error("Not connected to Firebase.");
|
|
168
|
+
const text = await getDoc(chapter);
|
|
169
|
+
if (!text.exists())
|
|
170
|
+
throw Error("Chapter text does not exist.");
|
|
171
|
+
return text.data();
|
|
172
|
+
};
|
|
173
|
+
export const updateChapterTextInFirestore = async (edition, chapter, text) => {
|
|
174
|
+
if (!db)
|
|
175
|
+
throw Error("Can't update chapter, not connected to Firebase.");
|
|
176
|
+
// Get the object for the book so we can store it.
|
|
177
|
+
const spec = { text: text };
|
|
178
|
+
// Update the book's preview
|
|
179
|
+
await setDoc(doc(db, `editions/${edition.id}/chapters`, chapter.id), spec);
|
|
180
|
+
};
|
|
181
|
+
export const subdomainIsAvailable = async (subdomain, book) => {
|
|
182
|
+
if (!db)
|
|
183
|
+
throw Error("Not connected to Firebase.");
|
|
184
|
+
const matches = await getDocs(query(collection(db, "books"), where("domain", "==", subdomain)));
|
|
185
|
+
return matches.empty || matches.docs[0].id === book.getRefID();
|
|
186
|
+
};
|
|
187
|
+
export const getBookIDFromBookName = async (subdomain) => {
|
|
188
|
+
if (!db)
|
|
189
|
+
throw Error("Can't find book ID from URL, not connected to Firebase.");
|
|
190
|
+
const matches = await getDocs(query(collection(db, "books"), where("domain", "==", subdomain)));
|
|
191
|
+
if (!matches.empty)
|
|
192
|
+
return matches.docs[0].id;
|
|
193
|
+
throw Error("No book by this name.");
|
|
194
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DocumentReference } from "firebase/firestore";
|
|
2
|
+
import BookMedia from "./BookMedia";
|
|
3
|
+
import Edition, { BookSaveStatus } from "./Edition";
|
|
4
|
+
export declare type Revision = {
|
|
5
|
+
ref: DocumentReference;
|
|
6
|
+
time: number;
|
|
7
|
+
summary: string;
|
|
8
|
+
published: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare type BookSpecification = {
|
|
11
|
+
title: string;
|
|
12
|
+
authors: string[];
|
|
13
|
+
description: string;
|
|
14
|
+
cover: string | null;
|
|
15
|
+
revisions: Revision[];
|
|
16
|
+
domain?: string;
|
|
17
|
+
uids: string[];
|
|
18
|
+
};
|
|
19
|
+
export default class Book {
|
|
20
|
+
readonly ref: DocumentReference;
|
|
21
|
+
readonly spec: BookSpecification;
|
|
22
|
+
edits: {
|
|
23
|
+
resolve: Function;
|
|
24
|
+
reject: Function;
|
|
25
|
+
}[];
|
|
26
|
+
lastEdit: number;
|
|
27
|
+
listeners: Set<Function>;
|
|
28
|
+
media: BookMedia;
|
|
29
|
+
constructor(ref: DocumentReference, spec: BookSpecification);
|
|
30
|
+
saveEdits(): void;
|
|
31
|
+
getMedia(): BookMedia;
|
|
32
|
+
requestSave(): Promise<void>;
|
|
33
|
+
addListener(listener: (status: BookSaveStatus) => void): void;
|
|
34
|
+
removeListener(listener: Function): void;
|
|
35
|
+
notifyListeners(status: BookSaveStatus): void;
|
|
36
|
+
toObject(): BookSpecification;
|
|
37
|
+
getSubdomain(): string | undefined;
|
|
38
|
+
setSubdomain(domain: string): Promise<void>;
|
|
39
|
+
getTitle(): string;
|
|
40
|
+
setTitle(title: string): Promise<void>;
|
|
41
|
+
getDescription(): string;
|
|
42
|
+
setDescription(description: string): Promise<void>;
|
|
43
|
+
getCover(): string | null;
|
|
44
|
+
setCover(cover: string | null): Promise<void>;
|
|
45
|
+
getAuthors(): string[];
|
|
46
|
+
setAuthors(authors: string[]): Promise<void>;
|
|
47
|
+
getRef(): DocumentReference<import("@firebase/firestore").DocumentData>;
|
|
48
|
+
getRefID(): string;
|
|
49
|
+
getDraftEdition(): undefined | Promise<Edition>;
|
|
50
|
+
isValidEditionNumber(edition: number): boolean;
|
|
51
|
+
getEditionNumber(number: number): Promise<Edition> | undefined;
|
|
52
|
+
getLatestPublishedEdition(): undefined | Promise<Edition>;
|
|
53
|
+
getLatestPublishedEditionID(): string | undefined;
|
|
54
|
+
getRevisions(): Revision[];
|
|
55
|
+
setRevisions(revisions: Revision[]): Promise<void>;
|
|
56
|
+
setEditionChangeSummary(summary: string, index: number): Promise<void> | undefined;
|
|
57
|
+
setPublished(published: boolean, revisionNumber: number): Promise<void> | undefined;
|
|
58
|
+
hasPublishedEdition(): boolean;
|
|
59
|
+
updateMetadataFromEdition(edition: Edition): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { loadEditionFromFirestore, updateBookInFirestore } from "../Firestore";
|
|
2
|
+
import BookMedia from "./BookMedia";
|
|
3
|
+
import Edition, { BookSaveStatus } from "./Edition";
|
|
4
|
+
export default class Book {
|
|
5
|
+
ref;
|
|
6
|
+
spec;
|
|
7
|
+
// A list of requested edits, stored as promises, resolved
|
|
8
|
+
// when the book is ready to save.
|
|
9
|
+
edits = [];
|
|
10
|
+
lastEdit = 0;
|
|
11
|
+
listeners = new Set();
|
|
12
|
+
media;
|
|
13
|
+
constructor(ref, spec) {
|
|
14
|
+
this.ref = ref;
|
|
15
|
+
this.spec = spec;
|
|
16
|
+
this.media = new BookMedia(this);
|
|
17
|
+
}
|
|
18
|
+
saveEdits() {
|
|
19
|
+
// If it's been more than a second since our last edit and there
|
|
20
|
+
// are edits that haven't been saved, try updating the book,
|
|
21
|
+
// and if we succeed, resolve all of the edits, and if we fail,
|
|
22
|
+
// then reject them.
|
|
23
|
+
if (Date.now() - this.lastEdit > 1000 && this.edits.length > 0) {
|
|
24
|
+
// Tell listeners that this book model changed.
|
|
25
|
+
this.notifyListeners(BookSaveStatus.Saving);
|
|
26
|
+
updateBookInFirestore(this)
|
|
27
|
+
.then(() => {
|
|
28
|
+
// Approve the edits.
|
|
29
|
+
this.edits.forEach(edit => edit.resolve());
|
|
30
|
+
this.notifyListeners(BookSaveStatus.Saved);
|
|
31
|
+
})
|
|
32
|
+
.catch(() => {
|
|
33
|
+
// Reject the edits.
|
|
34
|
+
this.edits.forEach(edit => edit.reject());
|
|
35
|
+
this.notifyListeners(BookSaveStatus.Error);
|
|
36
|
+
})
|
|
37
|
+
.finally(() => {
|
|
38
|
+
// Reset the edit queue.
|
|
39
|
+
this.edits = [];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
getMedia() { return this.media; }
|
|
44
|
+
// Adds a save request to the queue, to be resolved later after a period of
|
|
45
|
+
// inactivity. Returns a promise that will eventually be resolved.
|
|
46
|
+
requestSave() {
|
|
47
|
+
// Tell listeners that this book model changed.
|
|
48
|
+
this.notifyListeners(BookSaveStatus.Changed);
|
|
49
|
+
// Return a promise that will resolve or reject later after this model saves the edits to the database.
|
|
50
|
+
this.lastEdit = Date.now();
|
|
51
|
+
const promise = new Promise((resolve, reject) => {
|
|
52
|
+
this.edits.push({ resolve: resolve, reject: reject });
|
|
53
|
+
});
|
|
54
|
+
return promise;
|
|
55
|
+
}
|
|
56
|
+
addListener(listener) { this.listeners.add(listener); }
|
|
57
|
+
removeListener(listener) { this.listeners.delete(listener); }
|
|
58
|
+
notifyListeners(status) { this.listeners.forEach(listener => listener.call(undefined, status)); }
|
|
59
|
+
toObject() { return this.spec; }
|
|
60
|
+
getSubdomain() { return this.spec.domain; }
|
|
61
|
+
setSubdomain(domain) {
|
|
62
|
+
if (domain.length === 0)
|
|
63
|
+
delete this.spec.domain;
|
|
64
|
+
else
|
|
65
|
+
this.spec.domain = domain;
|
|
66
|
+
return this.requestSave();
|
|
67
|
+
}
|
|
68
|
+
getTitle() { return this.spec.title; }
|
|
69
|
+
setTitle(title) {
|
|
70
|
+
this.spec.title = title;
|
|
71
|
+
return this.requestSave();
|
|
72
|
+
}
|
|
73
|
+
getDescription() { return this.spec.description; }
|
|
74
|
+
setDescription(description) {
|
|
75
|
+
this.spec.description = description;
|
|
76
|
+
return this.requestSave();
|
|
77
|
+
}
|
|
78
|
+
getCover() { return this.spec.cover; }
|
|
79
|
+
setCover(cover) {
|
|
80
|
+
this.spec.cover = cover;
|
|
81
|
+
return this.requestSave();
|
|
82
|
+
}
|
|
83
|
+
getAuthors() { return this.spec.authors; }
|
|
84
|
+
setAuthors(authors) {
|
|
85
|
+
this.spec.authors = authors;
|
|
86
|
+
return this.requestSave();
|
|
87
|
+
}
|
|
88
|
+
getRef() { return this.ref; }
|
|
89
|
+
getRefID() { return this.ref.id; }
|
|
90
|
+
getDraftEdition() {
|
|
91
|
+
return this.spec.revisions.length === 0 ? undefined : loadEditionFromFirestore(this, this.spec.revisions[0].ref.id);
|
|
92
|
+
}
|
|
93
|
+
isValidEditionNumber(edition) {
|
|
94
|
+
return this.getEditionNumber(edition) !== undefined;
|
|
95
|
+
}
|
|
96
|
+
/* Note: editions are numbered 1-n, but stored in reverse chronological order. */
|
|
97
|
+
getEditionNumber(number) {
|
|
98
|
+
return number < 1 || number > this.spec.revisions.length ?
|
|
99
|
+
undefined :
|
|
100
|
+
loadEditionFromFirestore(this, this.spec.revisions[this.spec.revisions.length - number].ref.id);
|
|
101
|
+
}
|
|
102
|
+
getLatestPublishedEdition() {
|
|
103
|
+
const latest = this.getLatestPublishedEditionID();
|
|
104
|
+
return latest === undefined ? undefined : loadEditionFromFirestore(this, latest);
|
|
105
|
+
}
|
|
106
|
+
getLatestPublishedEditionID() {
|
|
107
|
+
return this.spec.revisions.find(e => e.published)?.ref.id;
|
|
108
|
+
}
|
|
109
|
+
getRevisions() { return this.spec.revisions.slice(); }
|
|
110
|
+
setRevisions(revisions) {
|
|
111
|
+
this.spec.revisions = revisions.slice();
|
|
112
|
+
return this.requestSave();
|
|
113
|
+
}
|
|
114
|
+
setEditionChangeSummary(summary, index) {
|
|
115
|
+
if (index >= 0 && index < this.spec.revisions.length) {
|
|
116
|
+
this.spec.revisions[index].summary = summary;
|
|
117
|
+
return this.requestSave();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
setPublished(published, revisionNumber) {
|
|
121
|
+
if (revisionNumber >= 0 && revisionNumber < this.spec.revisions.length) {
|
|
122
|
+
this.spec.revisions[revisionNumber].published = published;
|
|
123
|
+
return this.requestSave();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
hasPublishedEdition() {
|
|
127
|
+
return this.spec.revisions.find(revision => revision.published) !== undefined;
|
|
128
|
+
}
|
|
129
|
+
updateMetadataFromEdition(edition) {
|
|
130
|
+
this.setTitle(edition.getTitle());
|
|
131
|
+
this.setCover(edition.getImage("cover") ?? null);
|
|
132
|
+
this.setAuthors(edition.getAuthors());
|
|
133
|
+
this.setDescription(edition.getDescription());
|
|
134
|
+
}
|
|
135
|
+
}
|