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.
Files changed (57) hide show
  1. package/dist/components/BookPreview.svelte +1 -0
  2. package/dist/components/chapter/Callout.svelte +0 -1
  3. package/dist/components/chapter/Embed.svelte +8 -6
  4. package/dist/components/chapter/ExtendedPrism.d.ts +1 -1
  5. package/dist/components/chapter/Figure.svelte +6 -1
  6. package/dist/components/chapter/Footnote.svelte +1 -0
  7. package/dist/components/chapter/List.svelte +16 -33
  8. package/dist/components/chapter/Marginal.svelte +4 -1
  9. package/dist/components/chapter/Paragraph.svelte +7 -2
  10. package/dist/components/chapter/Quote.svelte +6 -6
  11. package/dist/components/chapter/Table.svelte +4 -0
  12. package/dist/components/controls/DarkToggle.svelte +1 -1
  13. package/dist/components/editor/BookishEditor.svelte +21 -26
  14. package/dist/components/editor/Commands.js +14 -14
  15. package/dist/components/editor/ConfirmButton.svelte +8 -2
  16. package/dist/components/editor/EmbedEditor.svelte +3 -3
  17. package/dist/components/editor/ImageChooser.svelte +8 -27
  18. package/dist/components/editor/ImageThumbnail.svelte +65 -0
  19. package/dist/components/editor/ImageThumbnail.svelte.d.ts +19 -0
  20. package/dist/components/editor/Permissions.svelte +2 -0
  21. package/dist/components/editor/TextEditor.svelte +5 -2
  22. package/dist/components/editor/Toggle.svelte +1 -1
  23. package/dist/components/editor/Toolbar.svelte +2 -1
  24. package/dist/components/page/Chapter.svelte +1 -1
  25. package/dist/components/page/DefinitionView.svelte +43 -38
  26. package/dist/components/page/Edition.svelte +41 -68
  27. package/dist/components/page/EditionLoader.svelte +12 -3
  28. package/dist/components/page/Editions.svelte +1 -1
  29. package/dist/components/page/Header.svelte +73 -69
  30. package/dist/components/page/Media.svelte +5 -4
  31. package/dist/components/page/MediaPreview.svelte +8 -2
  32. package/dist/components/page/MediaPreview.svelte.d.ts +1 -0
  33. package/dist/components/page/Outline.svelte +1 -1
  34. package/dist/components/page/Page.svelte +6 -3
  35. package/dist/components/page/Reference.svelte +1 -1
  36. package/dist/components/page/Search.svelte +1 -1
  37. package/dist/components/page/TableOfContents.svelte +3 -6
  38. package/dist/components/page/TableOfContentsRow.svelte +1 -1
  39. package/dist/components/page/ThemeEditor.svelte +1 -1
  40. package/dist/models/CRUD.d.ts +2 -1
  41. package/dist/models/CRUD.js +18 -5
  42. package/dist/models/book/BookMedia.d.ts +3 -0
  43. package/dist/models/book/BookMedia.js +22 -5
  44. package/dist/models/book/Chapter.d.ts +9 -7
  45. package/dist/models/book/Chapter.js +16 -6
  46. package/dist/models/book/Edition.d.ts +6 -1
  47. package/dist/models/book/Edition.js +22 -10
  48. package/dist/models/book/Words.js +0 -1
  49. package/dist/models/chapter/ChapterNode.d.ts +1 -0
  50. package/dist/models/chapter/ChapterNode.js +3 -0
  51. package/dist/models/chapter/DefinitionNode.js +1 -1
  52. package/dist/models/chapter/FormatNode.js +4 -1
  53. package/dist/models/chapter/InlineCodeNode.js +1 -1
  54. package/dist/models/chapter/LinkNode.js +1 -1
  55. package/dist/models/chapter/ListNode.js +1 -1
  56. package/dist/models/chapter/Parser.js +3 -6
  57. package/package.json +25 -23
@@ -60,6 +60,7 @@ $: url =
60
60
  text-align: left;
61
61
  flex-wrap: wrap;
62
62
  height: 12em;
63
+ overflow-y: hidden;
63
64
  }
64
65
 
65
66
  .cover {
@@ -25,7 +25,6 @@ export let node;
25
25
  padding-bottom: var(--bookish-paragraph-spacing);
26
26
  line-height: var(--bookish-paragraph-line-height-tight);
27
27
  clear: both;
28
- position: relative;
29
28
  }
30
29
 
31
30
  .bookish-callout :global(h2),
@@ -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 = false;
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
- <p
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
- tabIndex="0"
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
- </p>
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 "prismjs";
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>
@@ -67,6 +67,7 @@ afterUpdate(() => $chapter?.layoutMarginals());
67
67
  font-weight: 300;
68
68
  font-size: var(--bookish-small-font-size);
69
69
  /* line-height: 1.5em; */
70
+ text-align: left;
70
71
  }
71
72
 
72
73
  .bookish-footnote-symbol {
@@ -24,50 +24,33 @@ export let node;
24
24
  line-height: var(--bookish-paragraph-line-height-tight);
25
25
  }
26
26
 
27
- ol {
28
- list-style: none;
29
- counter-reset: item;
27
+ li {
28
+ margin-bottom: calc(0.5 * var(--bookish-paragraph-spacing));
30
29
  }
31
30
 
32
- ol > li {
33
- counter-increment: item;
31
+ /* Lists are decimal numbered */
32
+ ol {
33
+ list-style: decimal;
34
34
  }
35
35
 
36
- ol > li {
37
- list-style-type: none;
36
+ /* First level is decimal */
37
+ ol li {
38
+ counter-increment: list-item;
39
+ list-style-type: decimal;
38
40
  }
39
41
 
40
- ol > :global(ol > li) {
41
- list-style-type: none;
42
+ /* Second level is lowercase alpha */
43
+ ol ol li {
44
+ list-style-type: lower-alpha;
42
45
  }
43
46
 
44
- ol > :global(ol > ol > li) {
45
- list-style-type: none;
47
+ /* Third level is uppercase alpha */
48
+ ol ol ol li {
49
+ list-style-type: upper-alpha;
46
50
  }
47
51
 
48
- ol > :global(li::before),
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
- tabIndex={editable ? null : 0}
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
- :global(.chapter-content) p:first-of-type:not(.placeholder)::first-letter {
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
- padding: 0;
41
- margin-top: var(--bookish-paragraph-spacing);
42
- margin-bottom: var(--bookish-paragraph-spacing);
43
- margin-left: var(--bookish-indent);
44
- margin-right: var(--bookish-indent);
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;
@@ -37,6 +37,10 @@ $: rows = node.getRows();
37
37
  tr.editable td,
38
38
  tr.editable th {
39
39
  border: 1px dotted var(--app-border-color);
40
+ }
41
+
42
+ td,
43
+ th {
40
44
  vertical-align: baseline;
41
45
  }
42
46
  </style>
@@ -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
- tabIndex="0"
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, getRoot } from '../page/Contexts';
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
- // Is the caret in relatively positioned ancestors above the editor? Undo their offsets.
163
- let caretAncestor = startNode.parentElement;
161
+ // Figure out the relative position of this editor.
162
+ let editorAncestor = element;
164
163
  let relativeX = 0;
165
164
  let relativeY = 0;
166
- let fixedX = undefined;
167
- let fixedY = undefined;
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 += caretAncestor.offsetLeft;
172
- relativeY += caretAncestor.offsetTop;
168
+ relativeX += editorAncestor.offsetLeft;
169
+ relativeY += editorAncestor.offsetTop;
173
170
  }
174
- else if (style.position === 'fixed') {
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
- // Loop through the commands to see if there's a match.
495
- const unmatched = commands.every((command) => {
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
- // Execute the command
514
- const handled = executeCommand(command, event.key);
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
- // Stop searching for a matching command.
520
- return handled;
512
+ // If not, this is not it.
513
+ else
514
+ return false;
521
515
  }
522
516
  // Continue searching for a matching command.
523
- return true;
517
+ return false;
524
518
  });
525
- if (unmatched) {
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
- tabIndex="0"
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].getTextNodes()[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)}>no</Button
13
- >&nbsp;<Button tooltip="yes" {command}>{confirm}</Button>
12
+ <Button tooltip="no" command={() => (confirming = false)}>cancel</Button
13
+ >&nbsp;<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.withURL('').withDescription('')
49
- : embed.withURL(image.url));
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, getCaret } from '../page/Contexts';
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
- tabIndex="0"
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
- <img
41
- class={`bookish-image-chooser-image ${
42
- image.url === selection ? 'selected' : ''
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 {};
@@ -28,6 +28,8 @@ async function addEditor() {
28
28
  }
29
29
  }
30
30
  function hasEditor(email) {
31
+ if (email.length === 0)
32
+ return false;
31
33
  const match = Array.from($emails.entries()).find(([, e]) => e === email);
32
34
  if (match === undefined)
33
35
  return false;
@@ -24,7 +24,7 @@ $: {
24
24
  // them with non-breaking spaces.
25
25
  const sizedText = showPlaceholder()
26
26
  ? placeholder
27
- : text.replace(/\s/g, '\u00a0');
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
- tabIndex="0"
27
+ tabindex="0"
28
28
  role="switch"
29
29
  aria-checked={on}
30
30
  on:keydown={(event) =>