@things-factory/gallery 10.1.248 → 10.1.250

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 (75) hide show
  1. package/dist-client/bootstrap.d.ts +2 -0
  2. package/dist-client/bootstrap.js +26 -0
  3. package/dist-client/bootstrap.js.map +1 -0
  4. package/dist-client/components/gallery-forms.d.ts +31 -0
  5. package/dist-client/components/gallery-forms.js +223 -0
  6. package/dist-client/components/gallery-forms.js.map +1 -0
  7. package/dist-client/components/gallery-saver-setting-let.d.ts +11 -0
  8. package/dist-client/components/gallery-saver-setting-let.js +81 -0
  9. package/dist-client/components/gallery-saver-setting-let.js.map +1 -0
  10. package/dist-client/components/gallery-show.d.ts +46 -0
  11. package/dist-client/components/gallery-show.js +316 -0
  12. package/dist-client/components/gallery-show.js.map +1 -0
  13. package/dist-client/components/gallery-uploader.d.ts +57 -0
  14. package/dist-client/components/gallery-uploader.js +624 -0
  15. package/dist-client/components/gallery-uploader.js.map +1 -0
  16. package/dist-client/components/gallery-viewer.d.ts +30 -0
  17. package/dist-client/components/gallery-viewer.js +429 -0
  18. package/dist-client/components/gallery-viewer.js.map +1 -0
  19. package/dist-client/index.d.ts +1 -0
  20. package/dist-client/index.js +2 -0
  21. package/dist-client/index.js.map +1 -0
  22. package/dist-client/pages/gallery-page.d.ts +54 -0
  23. package/dist-client/pages/gallery-page.js +590 -0
  24. package/dist-client/pages/gallery-page.js.map +1 -0
  25. package/dist-client/route.d.ts +1 -0
  26. package/dist-client/route.js +9 -0
  27. package/dist-client/route.js.map +1 -0
  28. package/dist-client/tsconfig.tsbuildinfo +1 -0
  29. package/dist-client/utils/gallery-api.d.ts +99 -0
  30. package/dist-client/utils/gallery-api.js +46 -0
  31. package/dist-client/utils/gallery-api.js.map +1 -0
  32. package/dist-client/utils/gallery-badge.d.ts +4 -0
  33. package/dist-client/utils/gallery-badge.js +45 -0
  34. package/dist-client/utils/gallery-badge.js.map +1 -0
  35. package/dist-client/utils/gallery-saver.d.ts +6 -0
  36. package/dist-client/utils/gallery-saver.js +69 -0
  37. package/dist-client/utils/gallery-saver.js.map +1 -0
  38. package/dist-client/utils/gallery-words.d.ts +7 -0
  39. package/dist-client/utils/gallery-words.js +20 -0
  40. package/dist-client/utils/gallery-words.js.map +1 -0
  41. package/dist-client/utils/photo-prep.d.ts +10 -0
  42. package/dist-client/utils/photo-prep.js +102 -0
  43. package/dist-client/utils/photo-prep.js.map +1 -0
  44. package/dist-server/attachment-reader.d.ts +1 -0
  45. package/dist-server/attachment-reader.js +13 -0
  46. package/dist-server/attachment-reader.js.map +1 -0
  47. package/dist-server/index.d.ts +1 -0
  48. package/dist-server/index.js +11 -0
  49. package/dist-server/index.js.map +1 -0
  50. package/dist-server/role-tasks.d.ts +2 -0
  51. package/dist-server/role-tasks.js +15 -0
  52. package/dist-server/role-tasks.js.map +1 -0
  53. package/dist-server/service/gallery/gallery-resolver.d.ts +29 -0
  54. package/dist-server/service/gallery/gallery-resolver.js +613 -0
  55. package/dist-server/service/gallery/gallery-resolver.js.map +1 -0
  56. package/dist-server/service/gallery/gallery-rules.d.ts +90 -0
  57. package/dist-server/service/gallery/gallery-rules.js +182 -0
  58. package/dist-server/service/gallery/gallery-rules.js.map +1 -0
  59. package/dist-server/service/gallery/gallery-service.d.ts +37 -0
  60. package/dist-server/service/gallery/gallery-service.js +234 -0
  61. package/dist-server/service/gallery/gallery-service.js.map +1 -0
  62. package/dist-server/service/gallery/gallery-types.d.ts +65 -0
  63. package/dist-server/service/gallery/gallery-types.js +239 -0
  64. package/dist-server/service/gallery/gallery-types.js.map +1 -0
  65. package/dist-server/service/gallery/gallery.d.ts +84 -0
  66. package/dist-server/service/gallery/gallery.js +335 -0
  67. package/dist-server/service/gallery/gallery.js.map +1 -0
  68. package/dist-server/service/gallery/index.d.ts +4 -0
  69. package/dist-server/service/gallery/index.js +8 -0
  70. package/dist-server/service/gallery/index.js.map +1 -0
  71. package/dist-server/service/index.d.ts +7 -0
  72. package/dist-server/service/index.js +23 -0
  73. package/dist-server/service/index.js.map +1 -0
  74. package/dist-server/tsconfig.tsbuildinfo +1 -0
  75. package/package.json +8 -8
@@ -0,0 +1,624 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ /*
3
+ * Posting photos (ADR-0165 decision 3) - and changing one's own post. Photos are all it needs; the caption, the tags
4
+ * and the event are there to add. When the photos were taken is read from the first one, and the events near that day
5
+ * are offered first - one press puts the post on one.
6
+ */
7
+ import '@material/web/button/filled-button.js';
8
+ import '@material/web/button/text-button.js';
9
+ import '@material/web/icon/icon.js';
10
+ import '@operato/input/ox-input-file.js';
11
+ import '@operato/input/ox-input-select-buttons.js';
12
+ import '@operato/popup/ox-popup-actions.js';
13
+ import { css, html, LitElement, nothing } from 'lit';
14
+ import { customElement, property, query, state } from 'lit/decorators.js';
15
+ import { i18next, localize } from '@operato/i18n';
16
+ import { notify } from '@operato/layout';
17
+ import { CommonPopupStyles, PopupBodyStyles } from '@operato/styles';
18
+ import { isMobileDevice } from '@operato/utils';
19
+ import { galleryApi, galleryChanged } from '../utils/gallery-api.js';
20
+ import { day, reasonOf, t } from '../utils/gallery-words.js';
21
+ import { shrunk, takenAtOf } from '../utils/photo-prep.js';
22
+ const MAX_PHOTOS = 20;
23
+ const MAX_CAPTION = 500;
24
+ const MAX_TAGS = 10;
25
+ const MAX_TAG = 30;
26
+ /* The events shown before 「more」 - the near ones are among them. */
27
+ const FIRST_CHOICES = 4;
28
+ /** yyyy-mm-dd of a date in the reader's own time. */
29
+ const dateText = (value) => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
30
+ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement) {
31
+ constructor() {
32
+ super(...arguments);
33
+ /** The photos the page picked before the window opened. */
34
+ this.files = [];
35
+ /** Where a new post goes - the screen it was opened from. */
36
+ this.space = 'COMPANY';
37
+ this.added = [];
38
+ this.kept = [];
39
+ this.caption = '';
40
+ this.tags = [];
41
+ this.tagText = '';
42
+ /** Why the tag being typed was not taken - said under the tags, the text kept so it can be shortened. */
43
+ this.tagRefusal = '';
44
+ this.suggestions = [];
45
+ this.chosenSpace = 'COMPANY';
46
+ /** A post of the own gallery is the person's alone until shared (decision 2) - off when posting. */
47
+ this.shared = false;
48
+ this.chosenEvent = null;
49
+ this.takenOn = dateText(new Date());
50
+ this.choices = [];
51
+ this.allChoices = false;
52
+ this.busy = false;
53
+ }
54
+ static { this.styles = [
55
+ CommonPopupStyles,
56
+ PopupBodyStyles,
57
+ css `
58
+ :host {
59
+ height: 100%;
60
+ }
61
+ .body {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--spacing-large, 12px);
65
+ }
66
+ h4 {
67
+ margin: 0 0 var(--spacing-small, 4px);
68
+ font-size: var(--ui-t-small, 12px);
69
+ font-weight: 600;
70
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
71
+ }
72
+ .photos {
73
+ display: grid;
74
+ grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
75
+ gap: var(--spacing-small, 4px);
76
+ }
77
+ .photo {
78
+ position: relative;
79
+ aspect-ratio: 1;
80
+ border-radius: var(--border-radius, 4px);
81
+ overflow: hidden;
82
+ background: var(--md-sys-color-surface-container-high);
83
+ }
84
+ .photo img {
85
+ width: 100%;
86
+ height: 100%;
87
+ object-fit: cover;
88
+ display: block;
89
+ }
90
+ .photo button {
91
+ position: absolute;
92
+ top: 2px;
93
+ inset-inline-end: 2px;
94
+ display: inline-flex;
95
+ padding: 2px;
96
+ border: 0;
97
+ border-radius: 50%;
98
+ background: rgb(0 0 0 / 55%);
99
+ color: #fff;
100
+ cursor: pointer;
101
+ }
102
+ .photo button md-icon {
103
+ --md-icon-size: 16px;
104
+ }
105
+ .add {
106
+ display: flex;
107
+ flex-direction: column;
108
+ align-items: center;
109
+ justify-content: center;
110
+ gap: 2px;
111
+ aspect-ratio: 1;
112
+ border: 1px dashed var(--ui-line, var(--md-sys-color-outline));
113
+ border-radius: var(--border-radius, 4px);
114
+ background: none;
115
+ color: var(--md-sys-color-primary);
116
+ font: inherit;
117
+ font-size: var(--ui-t-small, 12px);
118
+ cursor: pointer;
119
+ }
120
+ .add[disabled] {
121
+ opacity: 0.4;
122
+ cursor: default;
123
+ }
124
+ ox-input-file {
125
+ display: contents;
126
+ }
127
+ textarea,
128
+ input {
129
+ box-sizing: border-box;
130
+ width: 100%;
131
+ font: inherit;
132
+ font-size: var(--ui-t-body, 14px);
133
+ padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
134
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
135
+ border-radius: var(--border-radius, 4px);
136
+ background: var(--md-sys-color-surface);
137
+ color: var(--md-sys-color-on-surface);
138
+ }
139
+ textarea {
140
+ min-height: 64px;
141
+ resize: vertical;
142
+ }
143
+ input[type='date'] {
144
+ width: auto;
145
+ }
146
+ .count {
147
+ text-align: end;
148
+ font-size: var(--ui-t-small, 12px);
149
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
150
+ }
151
+ .refusal {
152
+ font-size: var(--ui-t-small, 12px);
153
+ color: var(--md-sys-color-error);
154
+ }
155
+ .chips {
156
+ display: flex;
157
+ flex-wrap: wrap;
158
+ gap: var(--spacing-small, 4px);
159
+ align-items: center;
160
+ }
161
+ .chip {
162
+ display: inline-flex;
163
+ align-items: center;
164
+ gap: 2px;
165
+ padding: 2px var(--spacing-medium, 8px);
166
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
167
+ border-radius: 999px;
168
+ background: none;
169
+ color: var(--md-sys-color-on-surface);
170
+ font: inherit;
171
+ font-size: var(--ui-t-small, 12px);
172
+ line-height: 1.6;
173
+ cursor: pointer;
174
+ }
175
+ .chip[aria-pressed='true'] {
176
+ border-color: var(--md-sys-color-primary);
177
+ background: var(--md-sys-color-primary-container);
178
+ color: var(--md-sys-color-on-primary-container);
179
+ }
180
+ .chip md-icon {
181
+ --md-icon-size: 14px;
182
+ }
183
+ .chip .near {
184
+ color: var(--md-sys-color-primary);
185
+ }
186
+ .tag-input {
187
+ flex: 1;
188
+ min-width: 120px;
189
+ width: auto;
190
+ border: 0;
191
+ padding: 2px 0;
192
+ background: none;
193
+ }
194
+ .tag-input:focus {
195
+ outline: none;
196
+ }
197
+ .tags {
198
+ padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
199
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
200
+ border-radius: var(--border-radius, 4px);
201
+ }
202
+ .suggest {
203
+ margin-top: var(--spacing-small, 4px);
204
+ }
205
+ .suggest .chip {
206
+ border-style: dashed;
207
+ }
208
+ .muted {
209
+ font-size: var(--ui-t-small, 12px);
210
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
211
+ }
212
+ .row {
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ gap: var(--spacing-large, 12px);
216
+ align-items: flex-end;
217
+ }
218
+ `
219
+ ]; }
220
+ render() {
221
+ const total = this.kept.length + this.added.length;
222
+ const full = total >= MAX_PHOTOS;
223
+ const company = this.chosenSpace === 'COMPANY';
224
+ const shown = this.allChoices ? this.choices : this.choices.slice(0, FIRST_CHOICES);
225
+ const chosen = this.choices.find(one => one.id === this.chosenEvent);
226
+ return html `<div class="body">
227
+ <section>
228
+ <div class="photos">
229
+ ${this.kept.map(one => html `<div class="photo">
230
+ <img src=${one.thumbnail || one.fullpath} alt="" />
231
+ <button
232
+ aria-label=${t('gallery.button.remove-photo')}
233
+ @click=${() => (this.kept = this.kept.filter(other => other !== one))}
234
+ >
235
+ <md-icon>close</md-icon>
236
+ </button>
237
+ </div>`)}
238
+ ${this.added.map(one => html `<div class="photo">
239
+ <img src=${one.url} alt="" />
240
+ <button aria-label=${t('gallery.button.remove-photo')} @click=${() => this.drop(one)}>
241
+ <md-icon>close</md-icon>
242
+ </button>
243
+ </div>`)}
244
+ <button class="add" ?disabled=${full || this.busy} @click=${() => this.picker?.pick()}>
245
+ <md-icon>add_photo_alternate</md-icon>${t('gallery.button.add-photos')}
246
+ </button>
247
+ ${isMobileDevice()
248
+ ? html `<button class="add" ?disabled=${full || this.busy} @click=${() => this.camera?.pick()}>
249
+ <md-icon>photo_camera</md-icon>${t('gallery.button.shoot-more')}
250
+ </button>`
251
+ : nothing}
252
+ </div>
253
+ <ox-input-file
254
+ id="pick"
255
+ quiet
256
+ outside-trigger
257
+ hide-filelist
258
+ multiple
259
+ accept="image/*"
260
+ .progress=${this.progress}
261
+ @change=${(e) => this.take(e)}
262
+ ></ox-input-file>
263
+ <ox-input-file
264
+ id="shoot"
265
+ quiet
266
+ outside-trigger
267
+ hide-filelist
268
+ accept="image/*"
269
+ capture="environment"
270
+ @change=${(e) => this.take(e)}
271
+ ></ox-input-file>
272
+ ${full ? html `<div class="muted">${t('gallery.text.photos-full', { count: MAX_PHOTOS })}</div>` : nothing}
273
+ </section>
274
+
275
+ <section>
276
+ <textarea
277
+ maxlength=${MAX_CAPTION}
278
+ placeholder=${t('gallery.text.caption-placeholder')}
279
+ .value=${this.caption}
280
+ @input=${(e) => (this.caption = e.target.value)}
281
+ ></textarea>
282
+ ${this.caption.length > MAX_CAPTION * 0.8 ? html `<div class="count">${this.caption.length} / ${MAX_CAPTION}</div>` : nothing}
283
+ </section>
284
+
285
+ <section>
286
+ <h4>${t('gallery.field.tags')}</h4>
287
+ <div class="chips tags">
288
+ ${this.tags.map(tag => html `<span class="chip">#${tag}<md-icon role="button" aria-label=${t('gallery.button.remove-tag')} @click=${() => (this.tags = this.tags.filter(one => one !== tag))}>close</md-icon></span>`)}
289
+ ${this.tags.length < MAX_TAGS
290
+ ? html `<input
291
+ class="tag-input"
292
+ .value=${this.tagText}
293
+ placeholder=${this.tags.length ? '' : t('gallery.text.tag-placeholder')}
294
+ @input=${(e) => this.typeTag(e.target.value)}
295
+ @keydown=${(e) => this.tagKey(e)}
296
+ @blur=${() => this.addTag(this.tagText)}
297
+ />`
298
+ : nothing}
299
+ </div>
300
+ ${this.tagRefusal ? html `<div class="refusal" role="alert">${this.tagRefusal}</div>` : nothing}
301
+ ${this.suggestions.length
302
+ ? html `<div class="chips suggest">
303
+ ${this.suggestions.filter(one => !this.tags.some(tag => tag.toLowerCase() === one.tag.toLowerCase())).map(one => html `<button class="chip" @mousedown=${(e) => e.preventDefault()} @click=${() => this.addTag(one.tag)}>#${one.tag}</button>`)}
304
+ </div>`
305
+ : nothing}
306
+ </section>
307
+
308
+ <section class="row">
309
+ <div>
310
+ <h4>${t('gallery.field.space')}</h4>
311
+ <ox-input-select-buttons
312
+ .options=${['COMPANY', 'PERSONAL'].map(space => ({ value: space, display: t(`gallery.space.${space}`) }))}
313
+ .value=${this.chosenSpace}
314
+ @change=${(e) => (e.detail ? this.chooseSpace(e.detail) : (e.target.value = this.chosenSpace))}
315
+ ></ox-input-select-buttons>
316
+ </div>
317
+ <div>
318
+ <h4>${t('gallery.field.taken-on')}</h4>
319
+ <input type="date" .value=${this.takenOn} @change=${(e) => this.changeDay(e.target.value)} />
320
+ </div>
321
+ </section>
322
+
323
+ ${company
324
+ ? html `<section>
325
+ <h4>${t('gallery.field.event')}</h4>
326
+ <div class="chips">
327
+ <button
328
+ class="chip"
329
+ aria-pressed=${String(!this.chosenEvent)}
330
+ @click=${() => (this.chosenEvent = null)}
331
+ >
332
+ ${t('gallery.text.no-event')}
333
+ </button>
334
+ ${chosen && !shown.includes(chosen) ? this.choiceChip(chosen) : nothing}
335
+ ${shown.map(one => this.choiceChip(one))}
336
+ ${this.choices.length > FIRST_CHOICES && !this.allChoices
337
+ ? html `<button class="chip" @click=${() => (this.allChoices = true)}>
338
+ ${t('gallery.button.more-events', { count: this.choices.length - FIRST_CHOICES })}
339
+ </button>`
340
+ : nothing}
341
+ </div>
342
+ </section>`
343
+ : html `<section>
344
+ <h4>${t('gallery.field.shared')}</h4>
345
+ <ox-input-select-buttons
346
+ .options=${[
347
+ { value: 'me', display: t('gallery.text.only-me') },
348
+ { value: 'everyone', display: t('gallery.text.everyone') }
349
+ ]}
350
+ .value=${this.shared ? 'everyone' : 'me'}
351
+ @change=${(e) => (e.detail ? (this.shared = e.detail === 'everyone') : (e.target.value = this.shared ? 'everyone' : 'me'))}
352
+ ></ox-input-select-buttons>
353
+ <div class="muted">${t(this.shared ? 'gallery.text.shared-note' : 'gallery.text.only-me-note')}</div>
354
+ </section>`}
355
+ </div>
356
+ <ox-popup-actions>
357
+ <md-text-button slot="cancel" ?disabled=${this.busy} @click=${() => this.close()}
358
+ >${t('gallery.button.cancel')}</md-text-button
359
+ >
360
+ <md-filled-button slot="confirm" ?disabled=${this.busy || total < 1} @click=${() => this.save()}
361
+ >${this.post ? t('gallery.button.save') : t('gallery.button.post-count', { count: total })}</md-filled-button
362
+ >
363
+ </ox-popup-actions>`;
364
+ }
365
+ choiceChip(one) {
366
+ return html `<button
367
+ class="chip"
368
+ aria-pressed=${String(this.chosenEvent === one.id)}
369
+ @click=${() => (this.chosenEvent = one.id)}
370
+ >
371
+ ${one.near ? html `<md-icon class="near">event_available</md-icon>` : nothing}${one.name}<span class="muted"
372
+ >· ${day(one.startAt)}</span
373
+ >
374
+ </button>`;
375
+ }
376
+ async firstUpdated() {
377
+ const post = this.post;
378
+ if (post) {
379
+ this.kept = [...post.photos];
380
+ this.caption = post.caption ?? '';
381
+ this.tags = [...(post.tags ?? [])];
382
+ this.chosenSpace = post.space;
383
+ this.shared = post.shared;
384
+ this.chosenEvent = post.event?.id ?? null;
385
+ this.takenOn = dateText(new Date(post.takenAt));
386
+ }
387
+ else {
388
+ this.chosenSpace = this.eventId ? 'COMPANY' : this.space;
389
+ this.chosenEvent = this.eventId ?? null;
390
+ }
391
+ await this.add(this.files ?? []);
392
+ await this.loadChoices();
393
+ }
394
+ disconnectedCallback() {
395
+ for (const one of this.added)
396
+ URL.revokeObjectURL(one.url);
397
+ clearTimeout(this.suggestTimer);
398
+ super.disconnectedCallback();
399
+ }
400
+ async take(e) {
401
+ const picked = [...(e.detail ?? [])];
402
+ /* The picker's own reset says `change` again, with nothing - that one is let go. */
403
+ if (!picked.length)
404
+ return;
405
+ e.target.reset?.();
406
+ const before = this.kept.length + this.added.length;
407
+ await this.add(picked);
408
+ /* The first photo says the day - the events near it come first. */
409
+ if (!before && !this.post)
410
+ await this.loadChoices();
411
+ }
412
+ async add(files) {
413
+ const room = MAX_PHOTOS - this.kept.length - this.added.length;
414
+ const photos = files
415
+ .filter(file => /^image\//i.test(file.type) || /\.(jpe?g|png|gif|webp|heic|heif)$/i.test(file.name))
416
+ .slice(0, Math.max(0, room));
417
+ if (photos.length < files.length)
418
+ notify({ level: 'warn', message: t('gallery.text.photos-skipped', { count: files.length - photos.length }) });
419
+ if (!photos.length)
420
+ return;
421
+ const first = !this.kept.length && !this.added.length && !this.post;
422
+ this.added = [...this.added, ...photos.map(file => ({ file, url: URL.createObjectURL(file) }))];
423
+ if (first) {
424
+ const takenAt = await takenAtOf(photos[0]);
425
+ if (takenAt)
426
+ this.takenOn = dateText(takenAt);
427
+ }
428
+ }
429
+ drop(one) {
430
+ URL.revokeObjectURL(one.url);
431
+ this.added = this.added.filter(other => other !== one);
432
+ }
433
+ takenAt() {
434
+ const [y, m, d] = this.takenOn.split('-').map(Number);
435
+ const now = new Date();
436
+ /* A day chosen without its hour keeps the hour it was posted - today's photo is not dated midnight. */
437
+ const at = y ? new Date(y, m - 1, d, now.getHours(), now.getMinutes()) : now;
438
+ return at.toISOString();
439
+ }
440
+ async loadChoices() {
441
+ try {
442
+ /* Offered, not chosen: the events near that day come first, and one press puts the post on one. */
443
+ this.choices = await galleryApi.eventChoices(this.takenAt());
444
+ }
445
+ catch {
446
+ this.choices = [];
447
+ }
448
+ }
449
+ async changeDay(value) {
450
+ if (!value)
451
+ return;
452
+ this.takenOn = value;
453
+ await this.loadChoices();
454
+ }
455
+ chooseSpace(space) {
456
+ this.chosenSpace = space;
457
+ if (space === 'PERSONAL')
458
+ this.chosenEvent = null;
459
+ }
460
+ typeTag(value) {
461
+ if (/[,\s]$/.test(value) && value.trim()) {
462
+ this.addTag(value);
463
+ return;
464
+ }
465
+ this.tagText = value;
466
+ clearTimeout(this.suggestTimer);
467
+ this.suggestTimer = setTimeout(async () => {
468
+ try {
469
+ this.suggestions = await galleryApi.tags(this.tagText);
470
+ }
471
+ catch {
472
+ this.suggestions = [];
473
+ }
474
+ }, 200);
475
+ }
476
+ tagKey(e) {
477
+ if (e.isComposing)
478
+ return;
479
+ if (e.key === 'Enter') {
480
+ e.preventDefault();
481
+ this.addTag(this.tagText);
482
+ }
483
+ else if (e.key === 'Backspace' && !this.tagText && this.tags.length) {
484
+ this.tags = this.tags.slice(0, -1);
485
+ }
486
+ }
487
+ /** Takes the typed tag, or says why not and keeps it - a longer tag was cut at 30 without a word (2026-10-30). */
488
+ addTag(raw) {
489
+ const tag = raw.replace(/[,]/g, ' ').trim().replace(/^#+/, '').replace(/\s+/g, ' ').trim();
490
+ if (tag.length > MAX_TAG) {
491
+ this.tagRefusal = t('gallery.text.tag-too-long-typed', { max: MAX_TAG, length: tag.length });
492
+ return false;
493
+ }
494
+ this.tagRefusal = '';
495
+ this.tagText = '';
496
+ if (!tag || this.tags.length >= MAX_TAGS || this.tags.some(one => one.toLowerCase() === tag.toLowerCase()))
497
+ return true;
498
+ this.tags = [...this.tags, tag];
499
+ return true;
500
+ }
501
+ async save() {
502
+ if (!this.addTag(this.tagText))
503
+ return;
504
+ this.busy = true;
505
+ try {
506
+ const files = await Promise.all(this.added.map(one => shrunk(one.file)));
507
+ const input = {
508
+ space: this.chosenSpace,
509
+ shared: this.chosenSpace === 'PERSONAL' && this.shared,
510
+ caption: this.caption.trim(),
511
+ takenAt: this.takenAt(),
512
+ tags: this.tags,
513
+ eventId: this.chosenSpace === 'COMPANY' ? this.chosenEvent : null
514
+ };
515
+ const uploading = (fraction) => (this.progress = fraction);
516
+ const post = this.post
517
+ ? await galleryApi.update(this.post.id, input, files, this.post.photos.filter(one => !this.kept.includes(one)).map(one => one.id), uploading)
518
+ : await galleryApi.create(input, files, uploading);
519
+ notify({ message: this.post ? t('gallery.text.saved') : t('gallery.text.posted') });
520
+ galleryChanged();
521
+ this.dispatchEvent(new CustomEvent('gallery-posted', { detail: post, bubbles: true, composed: true }));
522
+ this.close();
523
+ }
524
+ catch (error) {
525
+ notify({ level: 'error', message: reasonOf(error) });
526
+ }
527
+ finally {
528
+ this.progress = undefined;
529
+ this.busy = false;
530
+ }
531
+ }
532
+ close() {
533
+ this.dispatchEvent(new CustomEvent('done', { bubbles: true, composed: true }));
534
+ }
535
+ };
536
+ __decorate([
537
+ property({ attribute: false }),
538
+ __metadata("design:type", Object)
539
+ ], GalleryUploader.prototype, "post", void 0);
540
+ __decorate([
541
+ property({ attribute: false }),
542
+ __metadata("design:type", Array)
543
+ ], GalleryUploader.prototype, "files", void 0);
544
+ __decorate([
545
+ property(),
546
+ __metadata("design:type", String)
547
+ ], GalleryUploader.prototype, "space", void 0);
548
+ __decorate([
549
+ property(),
550
+ __metadata("design:type", Object)
551
+ ], GalleryUploader.prototype, "eventId", void 0);
552
+ __decorate([
553
+ state(),
554
+ __metadata("design:type", Array)
555
+ ], GalleryUploader.prototype, "added", void 0);
556
+ __decorate([
557
+ state(),
558
+ __metadata("design:type", Array)
559
+ ], GalleryUploader.prototype, "kept", void 0);
560
+ __decorate([
561
+ state(),
562
+ __metadata("design:type", Object)
563
+ ], GalleryUploader.prototype, "caption", void 0);
564
+ __decorate([
565
+ state(),
566
+ __metadata("design:type", Array)
567
+ ], GalleryUploader.prototype, "tags", void 0);
568
+ __decorate([
569
+ state(),
570
+ __metadata("design:type", Object)
571
+ ], GalleryUploader.prototype, "tagText", void 0);
572
+ __decorate([
573
+ state(),
574
+ __metadata("design:type", Object)
575
+ ], GalleryUploader.prototype, "tagRefusal", void 0);
576
+ __decorate([
577
+ state(),
578
+ __metadata("design:type", Array)
579
+ ], GalleryUploader.prototype, "suggestions", void 0);
580
+ __decorate([
581
+ state(),
582
+ __metadata("design:type", String)
583
+ ], GalleryUploader.prototype, "chosenSpace", void 0);
584
+ __decorate([
585
+ state(),
586
+ __metadata("design:type", Object)
587
+ ], GalleryUploader.prototype, "shared", void 0);
588
+ __decorate([
589
+ state(),
590
+ __metadata("design:type", Object)
591
+ ], GalleryUploader.prototype, "chosenEvent", void 0);
592
+ __decorate([
593
+ state(),
594
+ __metadata("design:type", Object)
595
+ ], GalleryUploader.prototype, "takenOn", void 0);
596
+ __decorate([
597
+ state(),
598
+ __metadata("design:type", Array)
599
+ ], GalleryUploader.prototype, "choices", void 0);
600
+ __decorate([
601
+ state(),
602
+ __metadata("design:type", Object)
603
+ ], GalleryUploader.prototype, "allChoices", void 0);
604
+ __decorate([
605
+ state(),
606
+ __metadata("design:type", Number)
607
+ ], GalleryUploader.prototype, "progress", void 0);
608
+ __decorate([
609
+ state(),
610
+ __metadata("design:type", Object)
611
+ ], GalleryUploader.prototype, "busy", void 0);
612
+ __decorate([
613
+ query('#pick'),
614
+ __metadata("design:type", Object)
615
+ ], GalleryUploader.prototype, "picker", void 0);
616
+ __decorate([
617
+ query('#shoot'),
618
+ __metadata("design:type", Object)
619
+ ], GalleryUploader.prototype, "camera", void 0);
620
+ GalleryUploader = __decorate([
621
+ customElement('gallery-uploader')
622
+ ], GalleryUploader);
623
+ export { GalleryUploader };
624
+ //# sourceMappingURL=gallery-uploader.js.map