Merge pull request 'Add alt text to place photos' (#71) from bugix/photo_alt_text into master
Reviewed-on: #71
This commit was merged in pull request #71.
This commit is contained in:
@@ -237,7 +237,7 @@ export default class PhotoCarousel extends Component {
|
||||
data-src={{photo.url}}
|
||||
class="place-header-photo
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage photo.url}}
|
||||
/>
|
||||
{{else if this.isGalleryThumbnails}}
|
||||
@@ -245,7 +245,7 @@ export default class PhotoCarousel extends Component {
|
||||
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
|
||||
class="place-header-photo
|
||||
{{if photo.isLandscape 'landscape' 'portrait'}}"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
|
||||
/>
|
||||
{{else}}
|
||||
@@ -260,7 +260,7 @@ export default class PhotoCarousel extends Component {
|
||||
<img
|
||||
data-src={{photo.url}}
|
||||
class="place-header-photo landscape"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage photo.url}}
|
||||
/>
|
||||
</picture>
|
||||
@@ -269,7 +269,7 @@ export default class PhotoCarousel extends Component {
|
||||
<img
|
||||
data-src={{if photo.thumbUrl photo.thumbUrl photo.url}}
|
||||
class="place-header-photo portrait"
|
||||
alt={{@name}}
|
||||
alt={{photo.alt}}
|
||||
{{fadeInImage (if photo.thumbUrl photo.thumbUrl photo.url)}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
@@ -27,6 +27,7 @@ export default class PlacePhotoUpload extends Component {
|
||||
@tracked isPublishing = false;
|
||||
@tracked isDragging = false;
|
||||
@tracked selectedTags = [];
|
||||
@tracked altText = '';
|
||||
|
||||
get place() {
|
||||
return this.args.place || {};
|
||||
@@ -103,6 +104,7 @@ export default class PlacePhotoUpload extends Component {
|
||||
this.file = null;
|
||||
this.uploadedPhoto = null;
|
||||
this.selectedTags = [];
|
||||
this.altText = '';
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(false);
|
||||
}
|
||||
@@ -118,6 +120,11 @@ export default class PlacePhotoUpload extends Component {
|
||||
this.selectedTags = [tag];
|
||||
}
|
||||
|
||||
@action
|
||||
updateAltText(event) {
|
||||
this.altText = event.target.value;
|
||||
}
|
||||
|
||||
deletePhotoTask = task(async (photoData) => {
|
||||
try {
|
||||
if (photoData.hash) {
|
||||
@@ -177,7 +184,10 @@ export default class PlacePhotoUpload extends Component {
|
||||
imeta.push(`dim ${photo.dim}`);
|
||||
}
|
||||
|
||||
imeta.push('alt A photo of a place');
|
||||
const alt = this.altText.trim();
|
||||
if (alt) {
|
||||
imeta.push(`alt ${alt}`);
|
||||
}
|
||||
|
||||
if (photo.fallbackUrls && photo.fallbackUrls.length > 0) {
|
||||
for (const fallbackUrl of photo.fallbackUrls) {
|
||||
@@ -209,6 +219,7 @@ export default class PlacePhotoUpload extends Component {
|
||||
// Clear out the file so user can upload more or be done
|
||||
this.file = null;
|
||||
this.uploadedPhoto = null;
|
||||
this.altText = '';
|
||||
|
||||
if (this.args.onUploadStateChange) {
|
||||
this.args.onUploadStateChange(false);
|
||||
@@ -235,13 +246,11 @@ export default class PlacePhotoUpload extends Component {
|
||||
{{/if}}
|
||||
|
||||
{{#if this.file}}
|
||||
<div class="photo-grid">
|
||||
<PlacePhotoUploadItem
|
||||
@file={{this.file}}
|
||||
@onSuccess={{this.handleUploadSuccess}}
|
||||
@onRemove={{this.removeFile}}
|
||||
/>
|
||||
</div>
|
||||
<PlacePhotoUploadItem
|
||||
@file={{this.file}}
|
||||
@onSuccess={{this.handleUploadSuccess}}
|
||||
@onRemove={{this.removeFile}}
|
||||
/>
|
||||
|
||||
{{#if this.suggestedTags.length}}
|
||||
<div class="photo-tag-suggestions">
|
||||
@@ -263,6 +272,19 @@ export default class PlacePhotoUpload extends Component {
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="photo-alt-input">Description (optional):</label>
|
||||
<input
|
||||
id="photo-alt-input"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Describe this photo"
|
||||
value={{this.altText}}
|
||||
disabled={{this.isPublishing}}
|
||||
{{on "input" this.updateAltText}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-publish"
|
||||
|
||||
+6
-8
@@ -245,13 +245,6 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.photo-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.photo-upload-item {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
@@ -259,6 +252,7 @@ body {
|
||||
overflow: hidden;
|
||||
background: #1e262e;
|
||||
width: 100%;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.photo-upload-item img {
|
||||
@@ -1866,8 +1860,12 @@ button.create-place {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.place-photo-upload .form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.photo-tag-suggestions {
|
||||
margin: 1rem 0 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.photo-tag-suggestions-title {
|
||||
|
||||
@@ -67,6 +67,7 @@ export function parsePlacePhotos(events) {
|
||||
let blurhash = null;
|
||||
let isLandscape = false;
|
||||
let aspectRatio = 16 / 9; // default
|
||||
let altText = null;
|
||||
let placeIdentifier = event.tags.find((t) => t[0] === 'i')?.[1];
|
||||
|
||||
for (const tag of imeta.slice(1)) {
|
||||
@@ -85,6 +86,10 @@ export function parsePlacePhotos(events) {
|
||||
isLandscape = true;
|
||||
}
|
||||
}
|
||||
} else if (tag.startsWith('alt ')) {
|
||||
const alt = tag.substring(4).trim();
|
||||
// Strip the legacy placeholder we used to write on every event
|
||||
altText = alt === 'A photo of a place' ? null : alt || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +105,7 @@ export function parsePlacePhotos(events) {
|
||||
aspectRatio,
|
||||
placeIdentifier,
|
||||
tags: eventTagValues,
|
||||
alt: altText,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+9
-8
@@ -2331,8 +2331,9 @@ packages:
|
||||
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||
engines: {node: ^4.5.0 || >= 5.9}
|
||||
|
||||
baseline-browser-mapping@2.9.14:
|
||||
resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
|
||||
baseline-browser-mapping@2.11.12:
|
||||
resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
basic-auth@2.0.1:
|
||||
@@ -2538,8 +2539,8 @@ packages:
|
||||
resolution: {integrity: sha512-RbsNrFyhwkx+6psk/0fK/Q9orOUr9VMxohGd8vTa4djf4TGLfblBgUfqZChrZuW0Q+mz2eBPFLusw9Jfukzmhg==}
|
||||
hasBin: true
|
||||
|
||||
caniuse-lite@1.0.30001764:
|
||||
resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==}
|
||||
caniuse-lite@1.0.30001809:
|
||||
resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==}
|
||||
|
||||
capture-exit@2.0.0:
|
||||
resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==}
|
||||
@@ -8420,7 +8421,7 @@ snapshots:
|
||||
|
||||
base64id@2.0.0: {}
|
||||
|
||||
baseline-browser-mapping@2.9.14: {}
|
||||
baseline-browser-mapping@2.11.12: {}
|
||||
|
||||
basic-auth@2.0.1:
|
||||
dependencies:
|
||||
@@ -8752,8 +8753,8 @@ snapshots:
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.9.14
|
||||
caniuse-lite: 1.0.30001764
|
||||
baseline-browser-mapping: 2.11.12
|
||||
caniuse-lite: 1.0.30001809
|
||||
electron-to-chromium: 1.5.267
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
@@ -8805,7 +8806,7 @@ snapshots:
|
||||
dependencies:
|
||||
tmp: 0.0.28
|
||||
|
||||
caniuse-lite@1.0.30001764: {}
|
||||
caniuse-lite@1.0.30001809: {}
|
||||
|
||||
capture-exit@2.0.0:
|
||||
dependencies:
|
||||
|
||||
@@ -97,4 +97,27 @@ module('Integration | Component | place-photo-upload', function (hooks) {
|
||||
|
||||
assert.dom('.photo-tag-suggestions').doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders an optional alt text input after upload selection', async function (assert) {
|
||||
this.place = {
|
||||
title: 'Cafe Alpha',
|
||||
osmId: '123',
|
||||
osmType: 'node',
|
||||
osmTags: { amenity: 'cafe' },
|
||||
};
|
||||
|
||||
await render(
|
||||
<template><PlacePhotoUpload @place={{this.place}} /></template>
|
||||
);
|
||||
|
||||
assert.dom('#photo-alt-input').doesNotExist();
|
||||
|
||||
const file = new File(['test'], 'photo.jpg', { type: 'image/jpeg' });
|
||||
await selectFile(this.element, file);
|
||||
|
||||
assert.dom('#photo-alt-input').exists();
|
||||
assert
|
||||
.dom('#photo-alt-input')
|
||||
.hasAttribute('placeholder', 'Describe this photo');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +148,68 @@ module('Unit | Utility | nostr', function () {
|
||||
assert.strictEqual(photos[1].placeIdentifier, 'osm:node:456');
|
||||
});
|
||||
|
||||
test('parsePlacePhotos extracts alt text from imeta', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [
|
||||
[
|
||||
'imeta',
|
||||
'url https://example.com/photo.jpg',
|
||||
'dim 800x600',
|
||||
'alt A sunny terrace overlooking the bay',
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 1);
|
||||
assert.strictEqual(photos[0].alt, 'A sunny terrace overlooking the bay');
|
||||
});
|
||||
|
||||
test('parsePlacePhotos strips the legacy placeholder alt text', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [
|
||||
[
|
||||
'imeta',
|
||||
'url https://example.com/photo.jpg',
|
||||
'dim 800x600',
|
||||
'alt A photo of a place',
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 1);
|
||||
assert.strictEqual(photos[0].alt, null);
|
||||
});
|
||||
|
||||
test('parsePlacePhotos leaves alt null when not provided', function (assert) {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-1',
|
||||
pubkey: 'pubkey-1',
|
||||
created_at: 100,
|
||||
tags: [['imeta', 'url https://example.com/photo.jpg', 'dim 800x600']],
|
||||
},
|
||||
];
|
||||
|
||||
const photos = parsePlacePhotos(events);
|
||||
|
||||
assert.strictEqual(photos.length, 1);
|
||||
assert.strictEqual(photos[0].alt, null);
|
||||
});
|
||||
|
||||
test('uniqNormalizedRelays returns normalized unique relays', function (assert) {
|
||||
const relays = uniqNormalizedRelays([
|
||||
'Relay.example.com',
|
||||
|
||||
Reference in New Issue
Block a user