bookish-press 0.5.2 → 0.5.5
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/dist/components/BookPreview.svelte +1 -0
- package/dist/components/chapter/Callout.svelte +0 -1
- package/dist/components/chapter/Embed.svelte +8 -6
- package/dist/components/chapter/ExtendedPrism.d.ts +1 -1
- package/dist/components/chapter/Figure.svelte +6 -1
- package/dist/components/chapter/Footnote.svelte +1 -0
- package/dist/components/chapter/List.svelte +16 -33
- package/dist/components/chapter/Marginal.svelte +4 -1
- package/dist/components/chapter/Paragraph.svelte +7 -2
- package/dist/components/chapter/Quote.svelte +6 -6
- package/dist/components/chapter/Table.svelte +4 -0
- package/dist/components/controls/DarkToggle.svelte +1 -1
- package/dist/components/editor/BookishEditor.svelte +21 -26
- package/dist/components/editor/Commands.js +14 -14
- package/dist/components/editor/ConfirmButton.svelte +8 -2
- package/dist/components/editor/EmbedEditor.svelte +3 -3
- package/dist/components/editor/ImageChooser.svelte +8 -27
- package/dist/components/editor/ImageThumbnail.svelte +65 -0
- package/dist/components/editor/ImageThumbnail.svelte.d.ts +19 -0
- package/dist/components/editor/Permissions.svelte +2 -0
- package/dist/components/editor/TextEditor.svelte +5 -2
- package/dist/components/editor/Toggle.svelte +1 -1
- package/dist/components/editor/Toolbar.svelte +2 -1
- package/dist/components/page/Chapter.svelte +1 -1
- package/dist/components/page/DefinitionView.svelte +43 -38
- package/dist/components/page/Edition.svelte +41 -68
- package/dist/components/page/EditionLoader.svelte +12 -3
- package/dist/components/page/Editions.svelte +1 -1
- package/dist/components/page/Header.svelte +73 -69
- package/dist/components/page/Media.svelte +5 -4
- package/dist/components/page/MediaPreview.svelte +8 -2
- package/dist/components/page/MediaPreview.svelte.d.ts +1 -0
- package/dist/components/page/Outline.svelte +1 -1
- package/dist/components/page/Page.svelte +6 -3
- package/dist/components/page/Reference.svelte +1 -1
- package/dist/components/page/Search.svelte +1 -1
- package/dist/components/page/TableOfContents.svelte +3 -6
- package/dist/components/page/TableOfContentsRow.svelte +1 -1
- package/dist/components/page/ThemeEditor.svelte +1 -1
- package/dist/models/CRUD.d.ts +2 -1
- package/dist/models/CRUD.js +18 -5
- package/dist/models/book/BookMedia.d.ts +3 -0
- package/dist/models/book/BookMedia.js +22 -5
- package/dist/models/book/Chapter.d.ts +9 -7
- package/dist/models/book/Chapter.js +16 -6
- package/dist/models/book/Edition.d.ts +6 -1
- package/dist/models/book/Edition.js +22 -10
- package/dist/models/book/Words.js +0 -1
- package/dist/models/chapter/ChapterNode.d.ts +1 -0
- package/dist/models/chapter/ChapterNode.js +3 -0
- package/dist/models/chapter/DefinitionNode.js +1 -1
- package/dist/models/chapter/FormatNode.js +4 -1
- package/dist/models/chapter/InlineCodeNode.js +1 -1
- package/dist/models/chapter/LinkNode.js +1 -1
- package/dist/models/chapter/ListNode.js +1 -1
- package/dist/models/chapter/Parser.js +3 -6
- package/package.json +25 -23
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<svelte:options immutable={true} />
|
|
2
2
|
|
|
3
3
|
<script>import { storage } from '../../models/Firebase';
|
|
4
|
-
import { getCaret, getBook, getBase } from '../page/Contexts';
|
|
4
|
+
import { getCaret, getBook, getBase, isEditionEditable, } from '../page/Contexts';
|
|
5
5
|
import Figure from './Figure.svelte';
|
|
6
6
|
import { getContext } from 'svelte';
|
|
7
7
|
export let node;
|
|
8
|
-
export let editable =
|
|
8
|
+
export let editable = true;
|
|
9
9
|
export let placeholder = '';
|
|
10
10
|
export let imageOnly = false;
|
|
11
11
|
$: url = node.getURL();
|
|
@@ -13,6 +13,7 @@ $: description = node.getDescription();
|
|
|
13
13
|
let caret = getCaret();
|
|
14
14
|
let book = getBook();
|
|
15
15
|
let base = getBase();
|
|
16
|
+
let editionEditable = isEditionEditable();
|
|
16
17
|
// Bookish editor should have passed down a way to set the active editor.
|
|
17
18
|
let claimCaret = getContext('claimeditor');
|
|
18
19
|
let dragging = false;
|
|
@@ -94,13 +95,14 @@ function handleClick() {
|
|
|
94
95
|
credit={imageOnly ? undefined : node.getCredit()}
|
|
95
96
|
>
|
|
96
97
|
{#if url.trim().length === 0}
|
|
97
|
-
{#if editable}
|
|
98
|
-
<
|
|
98
|
+
{#if editable && editionEditable}
|
|
99
|
+
<div
|
|
100
|
+
role="button"
|
|
99
101
|
class={`bookish-figure-unspecified ${
|
|
100
102
|
dragging ? 'bookish-figure-dragging' : ''
|
|
101
103
|
}`}
|
|
102
104
|
on:click|preventDefault={handleClick}
|
|
103
|
-
|
|
105
|
+
tabindex="0"
|
|
104
106
|
on:keydown={(event) =>
|
|
105
107
|
event.key === ' ' || event.key === 'Enter'
|
|
106
108
|
? handleClick()
|
|
@@ -113,7 +115,7 @@ function handleClick() {
|
|
|
113
115
|
: placeholder.length > 0
|
|
114
116
|
? placeholder
|
|
115
117
|
: 'Click or drag to choose or upload an image, or enter an image or video URL.'}
|
|
116
|
-
</
|
|
118
|
+
</div>
|
|
117
119
|
{:else}
|
|
118
120
|
<p class="bookish-figure-unspecified">No image or video</p>
|
|
119
121
|
{/if}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export default Prism;
|
|
2
|
-
import Prism from
|
|
2
|
+
import Prism from 'prismjs';
|
|
@@ -32,6 +32,8 @@ function focusFigure() {
|
|
|
32
32
|
<figure class="bookish-figure" data-nodeid={node.nodeID}>
|
|
33
33
|
<div
|
|
34
34
|
class:selected
|
|
35
|
+
role="button"
|
|
36
|
+
tabindex="0"
|
|
35
37
|
on:mousedown={focusFigure}
|
|
36
38
|
on:keydown={(event) =>
|
|
37
39
|
event.key === ' ' || event.key === 'Enter'
|
|
@@ -70,7 +72,6 @@ function focusFigure() {
|
|
|
70
72
|
|
|
71
73
|
.caption {
|
|
72
74
|
text-align: center;
|
|
73
|
-
position: relative;
|
|
74
75
|
color: var(--bookish-muted-color);
|
|
75
76
|
line-height: var(--bookish-paragraph-line-height-tight);
|
|
76
77
|
font-size: var(--bookish-block-font-size);
|
|
@@ -87,4 +88,8 @@ function focusFigure() {
|
|
|
87
88
|
font-style: italic;
|
|
88
89
|
font-size: var(--bookish-small-font-size);
|
|
89
90
|
}
|
|
91
|
+
|
|
92
|
+
figcaption {
|
|
93
|
+
position: relative;
|
|
94
|
+
}
|
|
90
95
|
</style>
|
|
@@ -24,50 +24,33 @@ export let node;
|
|
|
24
24
|
line-height: var(--bookish-paragraph-line-height-tight);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
counter-reset: item;
|
|
27
|
+
li {
|
|
28
|
+
margin-bottom: calc(0.5 * var(--bookish-paragraph-spacing));
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
/* Lists are decimal numbered */
|
|
32
|
+
ol {
|
|
33
|
+
list-style: decimal;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/* First level is decimal */
|
|
37
|
+
ol li {
|
|
38
|
+
counter-increment: list-item;
|
|
39
|
+
list-style-type: decimal;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/* Second level is lowercase alpha */
|
|
43
|
+
ol ol li {
|
|
44
|
+
list-style-type: lower-alpha;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
/* Third level is uppercase alpha */
|
|
48
|
+
ol ol ol li {
|
|
49
|
+
list-style-type: upper-alpha;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
ol
|
|
49
|
-
ul > :global(ol > li::before),
|
|
50
|
-
ul > :global(ul > ol > li::before) {
|
|
51
|
-
content: counter(item) '. ';
|
|
52
|
+
ol li::marker {
|
|
52
53
|
color: var(--bookish-bullet-color);
|
|
53
|
-
display: inline-block;
|
|
54
|
-
width: 1em;
|
|
55
|
-
margin-left: -1.5rem;
|
|
56
|
-
margin-right: 0.5rem;
|
|
57
|
-
text-align: right;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
ol > :global(ol > li::before),
|
|
61
|
-
ul > :global(ol > ol > li::before) {
|
|
62
|
-
content: counter(item, lower-alpha) '. ';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
ol > :global(ol > ol > li:before) {
|
|
66
|
-
content: counter(item, upper-alpha) '. ';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
li {
|
|
70
|
-
margin-bottom: calc(0.5 * var(--bookish-paragraph-spacing));
|
|
71
54
|
}
|
|
72
55
|
|
|
73
56
|
ul li {
|
|
@@ -66,6 +66,8 @@ onMount(() => {
|
|
|
66
66
|
editable ? '' : 'interactive'
|
|
67
67
|
}`}
|
|
68
68
|
aria-label={label}
|
|
69
|
+
role="button"
|
|
70
|
+
tabindex="0"
|
|
69
71
|
on:mousedown={(event) => toggle(event, true)}
|
|
70
72
|
on:keydown={(event) =>
|
|
71
73
|
event.key === 'Enter' || event.key === ' '
|
|
@@ -77,7 +79,8 @@ onMount(() => {
|
|
|
77
79
|
class={'bookish-marginal' +
|
|
78
80
|
(isHidden ? ' bookish-marginal-hidden' : '') +
|
|
79
81
|
(hovered ? ' bookish-marginal-hovered' : '')}
|
|
80
|
-
|
|
82
|
+
role="button"
|
|
83
|
+
tabindex={editable ? null : 0}
|
|
81
84
|
on:mousedown={() => toggle(undefined, false)}
|
|
82
85
|
on:keydown={(event) =>
|
|
83
86
|
event.key === 'Enter' || event.key === ' '
|
|
@@ -19,10 +19,15 @@ $: classes =
|
|
|
19
19
|
($chapter && $chapter.highlightedID === id
|
|
20
20
|
? ' bookish-content-highlight'
|
|
21
21
|
: '');
|
|
22
|
+
$: firstParagraph = $root.getFirstParagraph();
|
|
23
|
+
$: dropcap =
|
|
24
|
+
placeholder === undefined &&
|
|
25
|
+
node === firstParagraph &&
|
|
26
|
+
/^[a-zA-Z]/.test(firstParagraph.getFirstTextNode().getText());
|
|
22
27
|
</script>
|
|
23
28
|
|
|
24
29
|
{#if level === 0}
|
|
25
|
-
<p data-nodeid={node.nodeID} class:placeholder
|
|
30
|
+
<p data-nodeid={node.nodeID} class:placeholder class:dropcap
|
|
26
31
|
><Format node={node.getFormat()} placeholder={placeholder ?? '¶'} /></p
|
|
27
32
|
>
|
|
28
33
|
{:else}
|
|
@@ -54,7 +59,7 @@ $: classes =
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/* This implements a drop cap in the first letter of the first text in the first format of the first paragraph of a chapter (but only chapters). */
|
|
57
|
-
|
|
62
|
+
p.dropcap::first-letter {
|
|
58
63
|
padding: 0 0.25rem;
|
|
59
64
|
margin: 0 0.25rem 0 0;
|
|
60
65
|
font-size: 4rem;
|
|
@@ -37,12 +37,11 @@ $: credit = node.getCredit();
|
|
|
37
37
|
font-family: var(--bookish-header-font-family);
|
|
38
38
|
font-weight: 400;
|
|
39
39
|
font-style: italic;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
position: relative;
|
|
40
|
+
margin: 0;
|
|
41
|
+
padding-top: 0;
|
|
42
|
+
padding-bottom: var(--bookish-paragraph-spacing);
|
|
43
|
+
padding-left: var(--bookish-indent);
|
|
44
|
+
padding-right: var(--bookish-indent);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
.quote > :global(p) {
|
|
@@ -52,6 +51,7 @@ $: credit = node.getCredit();
|
|
|
52
51
|
|
|
53
52
|
.caption {
|
|
54
53
|
margin-top: 0.5em;
|
|
54
|
+
margin-right: var(--bookish-indent);
|
|
55
55
|
text-align: right;
|
|
56
56
|
font-family: var(--bookish-header-font-family);
|
|
57
57
|
font-style: italic;
|
|
@@ -13,7 +13,7 @@ function change() {
|
|
|
13
13
|
class="dark-toggle"
|
|
14
14
|
role="button"
|
|
15
15
|
aria-label={dark === true ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
16
|
-
|
|
16
|
+
tabindex="0"
|
|
17
17
|
on:click={change}
|
|
18
18
|
on:keydown={(event) => {
|
|
19
19
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -16,9 +16,8 @@ import ErrorNode from '../../models/chapter/ErrorNode';
|
|
|
16
16
|
import { caretRangeToIndexRange, indexRangeToCaretRange, } from '../../models/chapter/Caret';
|
|
17
17
|
import commands from './Commands';
|
|
18
18
|
import { afterUpdate, onMount, setContext } from 'svelte';
|
|
19
|
-
import { getCaret
|
|
19
|
+
import { getCaret } from '../page/Contexts';
|
|
20
20
|
import CaretView from './CaretView.svelte';
|
|
21
|
-
import { writable } from 'svelte/store';
|
|
22
21
|
const IDLE_TIME = 500;
|
|
23
22
|
export let text;
|
|
24
23
|
export let parser;
|
|
@@ -159,25 +158,18 @@ afterUpdate(() => {
|
|
|
159
158
|
const lineHeight = lineHeightString.endsWith('px')
|
|
160
159
|
? parseInt(lineHeightString.substring(0, lineHeightString.length - 2))
|
|
161
160
|
: undefined;
|
|
162
|
-
//
|
|
163
|
-
let
|
|
161
|
+
// Figure out the relative position of this editor.
|
|
162
|
+
let editorAncestor = element;
|
|
164
163
|
let relativeX = 0;
|
|
165
164
|
let relativeY = 0;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
while (caretAncestor != null) {
|
|
169
|
-
const style = window.getComputedStyle(caretAncestor);
|
|
165
|
+
while (editorAncestor != null) {
|
|
166
|
+
const style = window.getComputedStyle(editorAncestor);
|
|
170
167
|
if (style.position === 'relative') {
|
|
171
|
-
relativeX +=
|
|
172
|
-
relativeY +=
|
|
168
|
+
relativeX += editorAncestor.offsetLeft;
|
|
169
|
+
relativeY += editorAncestor.offsetTop;
|
|
173
170
|
}
|
|
174
|
-
|
|
175
|
-
fixedX = caretAncestor.offsetLeft;
|
|
176
|
-
fixedY = caretAncestor.offsetTop;
|
|
177
|
-
}
|
|
178
|
-
caretAncestor = caretAncestor.parentElement;
|
|
171
|
+
editorAncestor = editorAncestor.parentElement;
|
|
179
172
|
}
|
|
180
|
-
// Is the caret in a fixed position marginal? Get it's position.
|
|
181
173
|
// If we're after a new line, calculate the correct position, since selections don't actually render to the next line.
|
|
182
174
|
const left = afterNewLine
|
|
183
175
|
? textRect.left
|
|
@@ -185,6 +177,7 @@ afterUpdate(() => {
|
|
|
185
177
|
const top = afterNewLine && lineHeight
|
|
186
178
|
? rangeRect.top + lineHeight
|
|
187
179
|
: rangeRect.top;
|
|
180
|
+
// Compute the final position of the caret.
|
|
188
181
|
const position = {
|
|
189
182
|
x: left + window.scrollX - relativeX,
|
|
190
183
|
y: top + window.scrollY - relativeY,
|
|
@@ -491,8 +484,8 @@ function handleKey(event, press = false) {
|
|
|
491
484
|
const context = getCaretContext();
|
|
492
485
|
if (context === undefined)
|
|
493
486
|
return;
|
|
494
|
-
//
|
|
495
|
-
const
|
|
487
|
+
// Find a command that handles the event.
|
|
488
|
+
const command = commands.find((command) => {
|
|
496
489
|
// If the keystroke and caret position matches the command signature, execute the command and update the caret range.
|
|
497
490
|
if ((command.press === undefined || command.press === press) &&
|
|
498
491
|
(command.shift === undefined ||
|
|
@@ -510,19 +503,20 @@ function handleKey(event, press = false) {
|
|
|
510
503
|
(command.active === true ||
|
|
511
504
|
(command.active instanceof Function &&
|
|
512
505
|
command.active.call(undefined, context, event.key)))) {
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
if (handled) {
|
|
506
|
+
// See if the command is handled
|
|
507
|
+
if (executeCommand(command, event.key)) {
|
|
516
508
|
event.preventDefault();
|
|
517
509
|
event.stopPropagation();
|
|
510
|
+
return true;
|
|
518
511
|
}
|
|
519
|
-
//
|
|
520
|
-
|
|
512
|
+
// If not, this is not it.
|
|
513
|
+
else
|
|
514
|
+
return false;
|
|
521
515
|
}
|
|
522
516
|
// Continue searching for a matching command.
|
|
523
|
-
return
|
|
517
|
+
return false;
|
|
524
518
|
});
|
|
525
|
-
if (
|
|
519
|
+
if (command === undefined) {
|
|
526
520
|
// Toolbar navigation
|
|
527
521
|
if (event.key === 'Escape') {
|
|
528
522
|
// If we've selected a FootnoteNode or Comment, navigate to the footnote or comment text to edit it.
|
|
@@ -813,13 +807,14 @@ setContext('claimeditor', claimActiveEditor);
|
|
|
813
807
|
class={`bookish-editor ${inAtom ? 'bookish-editor-atom-focused' : ''}`}
|
|
814
808
|
class:locked
|
|
815
809
|
aria-disabled={locked}
|
|
810
|
+
role="textbox"
|
|
816
811
|
bind:this={element}
|
|
817
812
|
on:keydown={handleKey}
|
|
818
813
|
on:keypress={handlePress}
|
|
819
814
|
on:mousedown|stopPropagation={handleMouseDown}
|
|
820
815
|
on:focus={handleFocus}
|
|
821
816
|
on:blur={handleBlur}
|
|
822
|
-
|
|
817
|
+
tabindex="0"
|
|
823
818
|
>
|
|
824
819
|
<!-- A view of the root -->
|
|
825
820
|
<svelte:component this={component} node={editedNode} {placeholder} />
|
|
@@ -835,19 +835,6 @@ const commands = [
|
|
|
835
835
|
return rootWithNode(context, context.blocks, edit.root, () => edit.range.start);
|
|
836
836
|
},
|
|
837
837
|
},
|
|
838
|
-
{
|
|
839
|
-
icon: IndentIcon,
|
|
840
|
-
description: 'indent list item',
|
|
841
|
-
category: 'list',
|
|
842
|
-
mutates: true,
|
|
843
|
-
control: false,
|
|
844
|
-
alt: false,
|
|
845
|
-
shift: false,
|
|
846
|
-
key: 'Tab',
|
|
847
|
-
visible: (context) => context.list !== undefined,
|
|
848
|
-
active: (context) => context.list !== undefined,
|
|
849
|
-
handler: (context) => rootWithNode(context, context.blocks, context.blocks?.withListsIndented(context.range, true)),
|
|
850
|
-
},
|
|
851
838
|
{
|
|
852
839
|
icon: UnindentIcon,
|
|
853
840
|
description: 'unindent list item',
|
|
@@ -862,6 +849,19 @@ const commands = [
|
|
|
862
849
|
context.list.isInside(context.root, ListNode),
|
|
863
850
|
handler: (context) => rootWithNode(context, context.blocks, context.blocks?.withListsIndented(context.range, false)),
|
|
864
851
|
},
|
|
852
|
+
{
|
|
853
|
+
icon: IndentIcon,
|
|
854
|
+
description: 'indent list item',
|
|
855
|
+
category: 'list',
|
|
856
|
+
mutates: true,
|
|
857
|
+
control: false,
|
|
858
|
+
alt: false,
|
|
859
|
+
shift: false,
|
|
860
|
+
key: 'Tab',
|
|
861
|
+
visible: (context) => context.list !== undefined,
|
|
862
|
+
active: (context) => context.list !== undefined,
|
|
863
|
+
handler: (context) => rootWithNode(context, context.blocks, context.blocks?.withListsIndented(context.range, true)),
|
|
864
|
+
},
|
|
865
865
|
{
|
|
866
866
|
icon: ClearIcon,
|
|
867
867
|
description: 'clear formatting',
|
|
@@ -1222,7 +1222,7 @@ const commands = [
|
|
|
1222
1222
|
const newTable = new TableNode(newRows, '|', new FormatNode('', [new TextNode()]));
|
|
1223
1223
|
return rootWithNode(context, context.blocks, context.blocks.withBlockInsertedBefore(context.paragraph, newTable), () => {
|
|
1224
1224
|
return {
|
|
1225
|
-
node: newTable.getRows()[0][0].
|
|
1225
|
+
node: newTable.getRows()[0][0].getFirstTextNode(),
|
|
1226
1226
|
index: 0,
|
|
1227
1227
|
};
|
|
1228
1228
|
});
|
|
@@ -9,8 +9,14 @@ let confirming = false;
|
|
|
9
9
|
{#if !confirming}
|
|
10
10
|
<Button {tooltip} command={() => (confirming = true)}><slot /></Button>
|
|
11
11
|
{:else}
|
|
12
|
-
<Button tooltip="no" command={() => (confirming = false)}>
|
|
13
|
-
> <Button
|
|
12
|
+
<Button tooltip="no" command={() => (confirming = false)}>cancel</Button
|
|
13
|
+
> <Button
|
|
14
|
+
tooltip="yes"
|
|
15
|
+
command={() => {
|
|
16
|
+
confirming = false;
|
|
17
|
+
command();
|
|
18
|
+
}}>{confirm}</Button
|
|
19
|
+
>
|
|
14
20
|
{/if}
|
|
15
21
|
</div>
|
|
16
22
|
|
|
@@ -43,10 +43,10 @@ function handleImageChange(event) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
function handleImageSelection(image) {
|
|
46
|
-
// Toggle the embed
|
|
47
46
|
$caret?.edit(embed, embed.getURL() === image.url
|
|
48
|
-
? embed
|
|
49
|
-
|
|
47
|
+
? // Toggle the embed if choosing the same image
|
|
48
|
+
embed.withURL('').withDescription('')
|
|
49
|
+
: embed.withURLs(image.url, image.thumb));
|
|
50
50
|
}
|
|
51
51
|
</script>
|
|
52
52
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script>import { onMount } from 'svelte';
|
|
2
|
-
import { getBook
|
|
2
|
+
import { getBook } from '../page/Contexts';
|
|
3
|
+
import ImageThumbnail from './ImageThumbnail.svelte';
|
|
3
4
|
export let select;
|
|
4
5
|
export let selection;
|
|
5
|
-
let editor = getCaret();
|
|
6
6
|
let book = getBook();
|
|
7
7
|
let images = undefined;
|
|
8
8
|
let expanded = false;
|
|
@@ -21,7 +21,8 @@ onMount(() => {
|
|
|
21
21
|
|
|
22
22
|
<div
|
|
23
23
|
class={`bookish-image-chooser ${expanded ? 'expanded' : ''}`}
|
|
24
|
-
|
|
24
|
+
tabindex="0"
|
|
25
|
+
role="button"
|
|
25
26
|
on:click={() => (expanded = !expanded)}
|
|
26
27
|
on:keydown={(event) =>
|
|
27
28
|
event.key === 'Enter' || event.key === ' '
|
|
@@ -37,18 +38,10 @@ onMount(() => {
|
|
|
37
38
|
<!-- Sort the images by their URL. There's probably a more meaningful sort,
|
|
38
39
|
such as placing unused images at the front of the list. -->
|
|
39
40
|
{#each images.sort((a, b) => a.url.localeCompare(b.url)) as image}
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
src={image.url}
|
|
45
|
-
alt={`Image named ${image.url}`}
|
|
46
|
-
tabIndex="0"
|
|
47
|
-
on:click|stopPropagation={() => select(image)}
|
|
48
|
-
on:keydown={(event) =>
|
|
49
|
-
event.key === 'Enter' || event.key === ' '
|
|
50
|
-
? select(image)
|
|
51
|
-
: undefined}
|
|
41
|
+
<ImageThumbnail
|
|
42
|
+
{image}
|
|
43
|
+
selected={image.url === selection}
|
|
44
|
+
{select}
|
|
52
45
|
/>
|
|
53
46
|
{:else}
|
|
54
47
|
No images uploaded
|
|
@@ -74,16 +67,4 @@ onMount(() => {
|
|
|
74
67
|
height: auto;
|
|
75
68
|
overflow-y: auto;
|
|
76
69
|
}
|
|
77
|
-
|
|
78
|
-
.bookish-image-chooser-image {
|
|
79
|
-
display: inline-block;
|
|
80
|
-
height: 2em;
|
|
81
|
-
cursor: pointer;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.bookish-image-chooser-image.selected {
|
|
85
|
-
outline: var(--app-chrome-border-size) solid
|
|
86
|
-
var(--app-interactive-color);
|
|
87
|
-
outline-offset: calc(-1 * var(--app-chrome-border-size));
|
|
88
|
-
}
|
|
89
70
|
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script>export let image;
|
|
2
|
+
export let selected;
|
|
3
|
+
export let select;
|
|
4
|
+
let retries = 3;
|
|
5
|
+
let error = false;
|
|
6
|
+
let loading = true;
|
|
7
|
+
function loadImage(url) {
|
|
8
|
+
retries--;
|
|
9
|
+
let img = new Image();
|
|
10
|
+
img.onerror = () => {
|
|
11
|
+
if (retries > 0) {
|
|
12
|
+
setTimeout(() => loadImage(url), 1000);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
loading = false;
|
|
16
|
+
error = true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
img.onload = () => {
|
|
20
|
+
loading = false;
|
|
21
|
+
};
|
|
22
|
+
img.src = url;
|
|
23
|
+
}
|
|
24
|
+
loadImage(image.thumb);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if loading}
|
|
28
|
+
<div class="thumbnail loading" />
|
|
29
|
+
{:else}
|
|
30
|
+
<div
|
|
31
|
+
tabindex="0"
|
|
32
|
+
role="button"
|
|
33
|
+
on:click|stopPropagation={() => select(image)}
|
|
34
|
+
on:keydown={(event) =>
|
|
35
|
+
event.key === 'Enter' || event.key === ' '
|
|
36
|
+
? select(image)
|
|
37
|
+
: undefined}
|
|
38
|
+
>
|
|
39
|
+
<img
|
|
40
|
+
class="thumbnail"
|
|
41
|
+
class:selected
|
|
42
|
+
src={error ? image.url : image.thumb}
|
|
43
|
+
alt={`Image named ${image.url}`}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
{/if}
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.thumbnail {
|
|
50
|
+
display: inline-block;
|
|
51
|
+
height: 2em;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.thumbnail.selected {
|
|
56
|
+
outline: var(--app-chrome-border-size) solid
|
|
57
|
+
var(--app-interactive-color);
|
|
58
|
+
outline-offset: calc(-1 * var(--app-chrome-border-size));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.loading {
|
|
62
|
+
width: 2.5em;
|
|
63
|
+
background: var(--app-chrome-background);
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import type { Image } from '../../models/book/BookMedia';
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: {
|
|
5
|
+
image: Image;
|
|
6
|
+
selected: boolean;
|
|
7
|
+
select: (image: Image) => void;
|
|
8
|
+
};
|
|
9
|
+
events: {
|
|
10
|
+
[evt: string]: CustomEvent<any>;
|
|
11
|
+
};
|
|
12
|
+
slots: {};
|
|
13
|
+
};
|
|
14
|
+
export type ImageThumbnailProps = typeof __propDef.props;
|
|
15
|
+
export type ImageThumbnailEvents = typeof __propDef.events;
|
|
16
|
+
export type ImageThumbnailSlots = typeof __propDef.slots;
|
|
17
|
+
export default class ImageThumbnail extends SvelteComponentTyped<ImageThumbnailProps, ImageThumbnailEvents, ImageThumbnailSlots> {
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -24,7 +24,7 @@ $: {
|
|
|
24
24
|
// them with non-breaking spaces.
|
|
25
25
|
const sizedText = showPlaceholder()
|
|
26
26
|
? placeholder
|
|
27
|
-
: text.replace(/\s
|
|
27
|
+
: text.replace(/\s$/g, '\u00a0');
|
|
28
28
|
// Clip the text to prevent this editor from getting too long when editing. If it goes past one line,
|
|
29
29
|
// the measurements and layout are way off.
|
|
30
30
|
const trimmedText = width !== undefined &&
|
|
@@ -111,7 +111,6 @@ function handleKeyPress(event) {
|
|
|
111
111
|
bind:this={field}
|
|
112
112
|
bind:value={text}
|
|
113
113
|
required
|
|
114
|
-
role="textbox"
|
|
115
114
|
aria-invalid={error !== undefined}
|
|
116
115
|
aria-label={label}
|
|
117
116
|
aria-placeholder={label}
|
|
@@ -136,6 +135,8 @@ function handleKeyPress(event) {
|
|
|
136
135
|
<style>
|
|
137
136
|
.text-editor {
|
|
138
137
|
position: relative;
|
|
138
|
+
display: inline-block;
|
|
139
|
+
max-width: 100%;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
input[type='text'] {
|
|
@@ -175,6 +176,8 @@ function handleKeyPress(event) {
|
|
|
175
176
|
|
|
176
177
|
/* This ensures the error message always appears below empty text editors */
|
|
177
178
|
.sizer {
|
|
179
|
+
display: inline-block;
|
|
180
|
+
max-width: 100%;
|
|
178
181
|
min-height: 1em;
|
|
179
182
|
opacity: 0;
|
|
180
183
|
border: none;
|
|
@@ -24,7 +24,7 @@ function toggle() {
|
|
|
24
24
|
class={`bookish-app-interactive ${
|
|
25
25
|
saving === Status.Saving ? ' bookish-text-editor-saving' : ''
|
|
26
26
|
} ${saving === Status.Error ? ' bookish-text-editor-error' : ''}`}
|
|
27
|
-
|
|
27
|
+
tabindex="0"
|
|
28
28
|
role="switch"
|
|
29
29
|
aria-checked={on}
|
|
30
30
|
on:keydown={(event) =>
|