feat: add ability to set focal points for media (#1303)
* feat: add ability to set focal points for media fixes #739 * fix tests * actually fix tests * really really fix tests * really really really fix tests pinkie swear
This commit is contained in:
parent
994dda4806
commit
85b75900c1
|
@ -52,5 +52,6 @@ module.exports = [
|
||||||
{ id: 'fa-clock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/clock-o.svg' },
|
{ id: 'fa-clock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/clock-o.svg' },
|
||||||
{ id: 'fa-refresh', src: 'src/thirdparty/font-awesome-svg-png/white/svg/refresh.svg' },
|
{ id: 'fa-refresh', src: 'src/thirdparty/font-awesome-svg-png/white/svg/refresh.svg' },
|
||||||
{ id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' },
|
{ id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' },
|
||||||
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' }
|
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' },
|
||||||
|
{ id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' }
|
||||||
]
|
]
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"@babel/core": "^7.5.0",
|
"@babel/core": "^7.5.0",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@webcomponents/custom-elements": "^1.2.4",
|
"@webcomponents/custom-elements": "^1.2.4",
|
||||||
|
"@wessberg/pointer-events": "^1.0.9",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"cheerio": "^1.0.0-rc.2",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { postStatus as postStatusToServer } from '../_api/statuses'
|
||||||
import { addStatusOrNotification } from './addStatusOrNotification'
|
import { addStatusOrNotification } from './addStatusOrNotification'
|
||||||
import { database } from '../_database/database'
|
import { database } from '../_database/database'
|
||||||
import { emit } from '../_utils/eventBus'
|
import { emit } from '../_utils/eventBus'
|
||||||
import { putMediaDescription } from '../_api/media'
|
import { putMediaMetadata } from '../_api/media'
|
||||||
|
|
||||||
export async function insertHandleForReply (statusId) {
|
export async function insertHandleForReply (statusId) {
|
||||||
let { currentInstance } = store.get()
|
let { currentInstance } = store.get()
|
||||||
|
@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
|
||||||
|
|
||||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
sensitive, spoilerText, visibility,
|
sensitive, spoilerText, visibility,
|
||||||
mediaDescriptions, inReplyToUuid, poll) {
|
mediaDescriptions, inReplyToUuid, poll, mediaFocalPoints) {
|
||||||
let { currentInstance, accessToken, online } = store.get()
|
let { currentInstance, accessToken, online } = store.get()
|
||||||
|
|
||||||
if (!online) {
|
if (!online) {
|
||||||
|
@ -31,17 +31,27 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
}
|
}
|
||||||
|
|
||||||
text = text || ''
|
text = text || ''
|
||||||
mediaDescriptions = mediaDescriptions || []
|
|
||||||
|
|
||||||
store.set({
|
let mediaMetadata = (mediaIds || []).map((mediaId, idx) => {
|
||||||
postingStatus: true
|
return {
|
||||||
|
description: mediaDescriptions && mediaDescriptions[idx],
|
||||||
|
focalPoint: mediaFocalPoints && mediaFocalPoints[idx]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
store.set({ postingStatus: true })
|
||||||
try {
|
try {
|
||||||
await Promise.all(mediaDescriptions.map(async (description, i) => {
|
await Promise.all(mediaMetadata.map(async ({ description, focalPoint }, i) => {
|
||||||
return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
|
description = description || ''
|
||||||
|
focalPoint = focalPoint || [0, 0]
|
||||||
|
focalPoint[0] = focalPoint[0] || 0
|
||||||
|
focalPoint[1] = focalPoint[1] || 0
|
||||||
|
if (description || focalPoint[0] || focalPoint[1]) {
|
||||||
|
return putMediaMetadata(currentInstance, accessToken, mediaIds[i], description, focalPoint)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
let status = await postStatusToServer(currentInstance, accessToken, text,
|
let status = await postStatusToServer(currentInstance, accessToken, text,
|
||||||
inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll)
|
inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll, mediaFocalPoints)
|
||||||
addStatusOrNotification(currentInstance, 'home', status)
|
addStatusOrNotification(currentInstance, 'home', status)
|
||||||
store.clearComposeData(realm)
|
store.clearComposeData(realm)
|
||||||
emit('postedStatus', realm, inReplyToUuid)
|
emit('postedStatus', realm, inReplyToUuid)
|
||||||
|
|
|
@ -11,7 +11,7 @@ export async function uploadMedia (instanceName, accessToken, file, description)
|
||||||
return post(url, formData, auth(accessToken), { timeout: MEDIA_WRITE_TIMEOUT })
|
return post(url, formData, auth(accessToken), { timeout: MEDIA_WRITE_TIMEOUT })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putMediaDescription (instanceName, accessToken, mediaId, description) {
|
export async function putMediaMetadata (instanceName, accessToken, mediaId, description, focus) {
|
||||||
let url = `${basename(instanceName)}/api/v1/media/${mediaId}`
|
let url = `${basename(instanceName)}/api/v1/media/${mediaId}`
|
||||||
return put(url, { description }, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
return put(url, { description, focus: (focus && focus.join(',')) }, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||||
}
|
}
|
||||||
|
|
102
src/routes/_components/Draggable.html
Normal file
102
src/routes/_components/Draggable.html
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<div class="draggable-area {draggableClass}"
|
||||||
|
on:pointermove="onPointerMove(event)"
|
||||||
|
on:pointerleave="onPointerLeave(event)"
|
||||||
|
on:click="onClick(event)"
|
||||||
|
ref:area
|
||||||
|
>
|
||||||
|
<div class="draggable-indicator {indicatorClass}"
|
||||||
|
style={indicatorStyle}
|
||||||
|
on:pointerdown="onPointerDown(event)"
|
||||||
|
on:pointerup="onPointerUp(event)"
|
||||||
|
ref:indicator
|
||||||
|
>
|
||||||
|
<div class="draggable-indicator-inner">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.draggable-area {
|
||||||
|
position: relative;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.draggable-indicator {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.draggable-indicator-inner {
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import { throttleRaf } from '../_utils/throttleRaf'
|
||||||
|
|
||||||
|
const clamp = x => Math.max(0, Math.min(1, x))
|
||||||
|
const throttledRaf = throttleRaf()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
draggableClass: '',
|
||||||
|
indicatorClass: '',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
indicatorWidth: 0,
|
||||||
|
indicatorHeight: 0
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
indicatorStyle: ({ x, y, indicatorWidth, indicatorHeight }) => (
|
||||||
|
`left: calc(${x * 100}% - ${indicatorWidth / 2}px); top: calc(${y * 100}% - ${indicatorHeight / 2}px);`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onPointerDown (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let rect = this.refs.indicator.getBoundingClientRect()
|
||||||
|
this.set({
|
||||||
|
dragging: true,
|
||||||
|
dragOffsetX: e.clientX - rect.left,
|
||||||
|
dragOffsetY: e.clientY - rect.top
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onPointerMove (e) {
|
||||||
|
if (this.get().dragging) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let { indicatorWidth, indicatorHeight, dragOffsetX, dragOffsetY } = this.get()
|
||||||
|
throttledRaf(() => {
|
||||||
|
let rect = this.refs.area.getBoundingClientRect()
|
||||||
|
let offsetX = dragOffsetX - (indicatorWidth / 2)
|
||||||
|
let offsetY = dragOffsetY - (indicatorHeight / 2)
|
||||||
|
let x = clamp((e.clientX - rect.left - offsetX) / rect.width)
|
||||||
|
let y = clamp((e.clientY - rect.top - offsetY) / rect.height)
|
||||||
|
this.set({ x, y })
|
||||||
|
this.fire('change', { x, y })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPointerUp (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
this.set({ dragging: false })
|
||||||
|
},
|
||||||
|
onPointerLeave (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
this.set({ dragging: false })
|
||||||
|
},
|
||||||
|
onClick (e) {
|
||||||
|
if (!e.target.classList.contains('draggable-indicator')) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let rect = this.refs.area.getBoundingClientRect()
|
||||||
|
let x = clamp((e.clientX - rect.left) / rect.width)
|
||||||
|
let y = clamp((e.clientY - rect.top) / rect.height)
|
||||||
|
this.set({ x, y })
|
||||||
|
this.fire('change', { x, y })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -65,7 +65,9 @@
|
||||||
// Here we do a pure css version instead of using
|
// Here we do a pure css version instead of using
|
||||||
// https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
// https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
||||||
|
|
||||||
if (!focus) return 'background-position: center;'
|
if (!focus) {
|
||||||
|
return 'background-position: center;'
|
||||||
|
}
|
||||||
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
|
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
|
||||||
},
|
},
|
||||||
fillFixSize: ({ forceSize, $largeInlineMedia }) => !$largeInlineMedia && !forceSize,
|
fillFixSize: ({ forceSize, $largeInlineMedia }) => !$largeInlineMedia && !forceSize,
|
||||||
|
|
|
@ -193,6 +193,7 @@
|
||||||
let sensitive = media.length && !!contentWarning
|
let sensitive = media.length && !!contentWarning
|
||||||
let mediaIds = media.map(_ => _.data.id)
|
let mediaIds = media.map(_ => _.data.id)
|
||||||
let mediaDescriptions = media.map(_ => _.description)
|
let mediaDescriptions = media.map(_ => _.description)
|
||||||
|
let mediaFocalPoints = media.map(_ => [_.focusX, _.focusY])
|
||||||
let inReplyTo = inReplyToId || ((realm === 'home' || realm === 'dialog') ? null : realm)
|
let inReplyTo = inReplyToId || ((realm === 'home' || realm === 'dialog') ? null : realm)
|
||||||
|
|
||||||
if (overLimit || (!text && !media.length)) {
|
if (overLimit || (!text && !media.length)) {
|
||||||
|
@ -217,7 +218,8 @@
|
||||||
/* no await */
|
/* no await */
|
||||||
postStatus(realm, text, inReplyTo, mediaIds,
|
postStatus(realm, text, inReplyTo, mediaIds,
|
||||||
sensitive, contentWarning, postPrivacyKey,
|
sensitive, contentWarning, postPrivacyKey,
|
||||||
mediaDescriptions, inReplyToUuid, pollToPost)
|
mediaDescriptions, inReplyToUuid, pollToPost,
|
||||||
|
mediaFocalPoints)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
<li class="compose-media compose-media-realm-{realm}">
|
<li class="compose-media compose-media-realm-{realm}" aria-label={shortName}>
|
||||||
<img
|
<img
|
||||||
|
alt=""
|
||||||
class="{type === 'audio' ? 'audio-preview' : ''}"
|
class="{type === 'audio' ? 'audio-preview' : ''}"
|
||||||
|
style="object-position: {objectPosition};"
|
||||||
src={previewSrc}
|
src={previewSrc}
|
||||||
{alt}
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<div class="compose-media-delete">
|
<div class="compose-media-buttons">
|
||||||
<button class="compose-media-delete-button"
|
<button class="compose-media-button compose-media-focal-button {type === 'audio' ? 'compose-media-hidden' : ''}"
|
||||||
aria-label="Delete {shortName}"
|
aria-hidden={type === 'audio'}
|
||||||
|
aria-label="Change preview"
|
||||||
|
title="Change preview"
|
||||||
|
on:click="onSetFocalPoint()" >
|
||||||
|
<SvgIcon className="compose-media-button-svg" href="#fa-crosshairs" />
|
||||||
|
</button>
|
||||||
|
<button class="compose-media-button compose-media-delete-button"
|
||||||
|
aria-label="Delete"
|
||||||
|
title="Delete"
|
||||||
on:click="onDeleteMedia()" >
|
on:click="onDeleteMedia()" >
|
||||||
<SvgIcon className="compose-media-delete-button-svg" href="#fa-times" />
|
<SvgIcon className="compose-media-button-svg" href="#fa-times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="compose-media-alt">
|
<div class="compose-media-alt">
|
||||||
|
@ -18,7 +28,9 @@
|
||||||
ref:textarea
|
ref:textarea
|
||||||
bind:value=rawText
|
bind:value=rawText
|
||||||
></textarea>
|
></textarea>
|
||||||
<label for="compose-media-input-{uuid}" class="sr-only">{label}</label>
|
<label for="compose-media-input-{uuid}" class="sr-only">
|
||||||
|
Describe for the visually impaired (image, video) or auditorily impaired (audio, video)
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<style>
|
<style>
|
||||||
|
@ -34,7 +46,6 @@
|
||||||
}
|
}
|
||||||
.compose-media img {
|
.compose-media img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
object-position: center center;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -60,33 +71,38 @@
|
||||||
.compose-media-alt-input:focus {
|
.compose-media-alt-input:focus {
|
||||||
background: var(--main-bg);
|
background: var(--main-bg);
|
||||||
}
|
}
|
||||||
.compose-media-delete {
|
.compose-media-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
.compose-media-delete-button {
|
.compose-media-button {
|
||||||
padding: 7px 10px 5px;
|
padding: 7px 10px 5px;
|
||||||
background: var(--floating-button-bg);
|
background: var(--floating-button-bg);
|
||||||
border: 1px solid var(--button-border);
|
border: 1px solid var(--button-border);
|
||||||
}
|
}
|
||||||
.compose-media-delete-button:hover {
|
.compose-media-button:hover {
|
||||||
background: var(--floating-button-bg-hover);
|
background: var(--floating-button-bg-hover);
|
||||||
}
|
}
|
||||||
.compose-media-delete-button:active {
|
.compose-media-button:active {
|
||||||
background: var(--floating-button-bg-active);
|
background: var(--floating-button-bg-active);
|
||||||
}
|
}
|
||||||
:global(.compose-media-delete-button-svg) {
|
:global(.compose-media-button-svg) {
|
||||||
fill: var(--button-text);
|
fill: var(--button-text);
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compose-media-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.audio-preview {
|
.audio-preview {
|
||||||
background: var(--audio-bg);
|
background: var(--audio-bg);
|
||||||
}
|
}
|
||||||
|
@ -113,6 +129,9 @@
|
||||||
import SvgIcon from '../SvgIcon.html'
|
import SvgIcon from '../SvgIcon.html'
|
||||||
import { autosize } from '../../_thirdparty/autosize/autosize'
|
import { autosize } from '../../_thirdparty/autosize/autosize'
|
||||||
import { ONE_TRANSPARENT_PIXEL } from '../../_static/media'
|
import { ONE_TRANSPARENT_PIXEL } from '../../_static/media'
|
||||||
|
import { get } from '../../_utils/lodash-lite'
|
||||||
|
import { coordsToPercent } from '../../_utils/coordsToPercent'
|
||||||
|
import { importMediaFocalPointDialog } from '../dialog/asyncDialogs'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
|
@ -124,24 +143,27 @@
|
||||||
this.teardownAutosize()
|
this.teardownAutosize()
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
rawText: ''
|
rawText: '',
|
||||||
|
focusX: 0,
|
||||||
|
focusY: 0
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
filename: ({ mediaItem }) => mediaItem.file && mediaItem.file.name,
|
type: ({ mediaItem }) => mediaItem.data.type,
|
||||||
alt: ({ filename, mediaItem }) => (
|
shortName: ({ mediaItem }) => (
|
||||||
// sometimes we no longer have the file, e.g. in a delete and redraft situation,
|
// sometimes we no longer have the file, e.g. in a delete and redraft situation,
|
||||||
// so fall back to the description if it was provided
|
// so fall back to the description if it was provided
|
||||||
filename || mediaItem.description || ''
|
get(mediaItem, ['file', 'name']) || get(mediaItem, ['description']) || 'media'
|
||||||
),
|
),
|
||||||
type: ({ mediaItem }) => mediaItem.data.type,
|
|
||||||
shortName: ({ filename }) => filename || 'media',
|
|
||||||
previewSrc: ({ mediaItem, type }) => (
|
previewSrc: ({ mediaItem, type }) => (
|
||||||
type === 'audio' ? ONE_TRANSPARENT_PIXEL : mediaItem.data.preview_url
|
type === 'audio' ? ONE_TRANSPARENT_PIXEL : mediaItem.data.preview_url
|
||||||
),
|
),
|
||||||
label: ({ shortName }) => (
|
uuid: ({ realm, mediaItem }) => `${realm}-${mediaItem.data.id}`,
|
||||||
`Describe ${shortName} for the visually impaired (image, video) or auditorily impaired (audio, video)`
|
objectPosition: ({ focusX, focusY }) => {
|
||||||
),
|
if (!focusX && !focusY) {
|
||||||
uuid: ({ realm, mediaItem }) => `${realm}-${mediaItem.data.id}`
|
return 'center center'
|
||||||
|
}
|
||||||
|
return `${coordsToPercent(focusX)}% ${100 - coordsToPercent(focusY)}%`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -150,10 +172,13 @@
|
||||||
this.observe('media', media => {
|
this.observe('media', media => {
|
||||||
media = media || []
|
media = media || []
|
||||||
let { index, rawText } = this.get()
|
let { index, rawText } = this.get()
|
||||||
let text = (media[index] && media[index].description) || ''
|
let text = get(media, [index, 'description'], '')
|
||||||
if (rawText !== text) {
|
if (rawText !== text) {
|
||||||
this.set({ rawText: text })
|
this.set({ rawText: text })
|
||||||
}
|
}
|
||||||
|
let focusX = get(media, [index, 'focusX'], 0)
|
||||||
|
let focusY = get(media, [index, 'focusY'], 0)
|
||||||
|
this.set({ focusX, focusY })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setupSyncToStore () {
|
setupSyncToStore () {
|
||||||
|
@ -161,12 +186,11 @@
|
||||||
|
|
||||||
this.observe('rawText', rawText => {
|
this.observe('rawText', rawText => {
|
||||||
let { realm, index, media } = this.get()
|
let { realm, index, media } = this.get()
|
||||||
if (media[index].description === rawText) {
|
if (media[index].description !== rawText) {
|
||||||
return
|
media[index].description = rawText
|
||||||
|
this.store.setComposeData(realm, { media })
|
||||||
|
saveStore()
|
||||||
}
|
}
|
||||||
media[index].description = rawText
|
|
||||||
this.store.setComposeData(realm, { media })
|
|
||||||
saveStore()
|
|
||||||
}, { init: false })
|
}, { init: false })
|
||||||
},
|
},
|
||||||
setupAutosize () {
|
setupAutosize () {
|
||||||
|
@ -176,11 +200,13 @@
|
||||||
autosize.destroy(this.refs.textarea)
|
autosize.destroy(this.refs.textarea)
|
||||||
},
|
},
|
||||||
onDeleteMedia () {
|
onDeleteMedia () {
|
||||||
let {
|
let { realm, index } = this.get()
|
||||||
realm,
|
|
||||||
index
|
|
||||||
} = this.get()
|
|
||||||
deleteMedia(realm, index)
|
deleteMedia(realm, index)
|
||||||
|
},
|
||||||
|
async onSetFocalPoint () {
|
||||||
|
let { realm, index } = this.get()
|
||||||
|
let showMediaFocalPointDialog = await importMediaFocalPointDialog()
|
||||||
|
showMediaFocalPointDialog(realm, index)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -43,3 +43,7 @@ export const importShowMuteDialog = () => import(
|
||||||
export const importShowReportDialog = () => import(
|
export const importShowReportDialog = () => import(
|
||||||
/* webpackChunkName: 'showReportDialog' */ './creators/showReportDialog'
|
/* webpackChunkName: 'showReportDialog' */ './creators/showReportDialog'
|
||||||
).then(getDefault)
|
).then(getDefault)
|
||||||
|
|
||||||
|
export const importMediaFocalPointDialog = () => import(
|
||||||
|
/* webpackChunkName: 'mediaFocalPointDialog' */ './creators/mediaFocalPointDialog'
|
||||||
|
).then(getDefault)
|
||||||
|
|
|
@ -0,0 +1,326 @@
|
||||||
|
<ModalDialog
|
||||||
|
{id}
|
||||||
|
{label}
|
||||||
|
{title}
|
||||||
|
background="var(--main-bg)"
|
||||||
|
className="media-focal-point-dialog"
|
||||||
|
on:show="measure()"
|
||||||
|
>
|
||||||
|
<form class="media-focal-point-container"
|
||||||
|
aria-label="Enter the focal point (X, Y) for this media"
|
||||||
|
on:resize="measure()"
|
||||||
|
>
|
||||||
|
<div class="media-focal-point-image-container" ref:container>
|
||||||
|
<img
|
||||||
|
{intrinsicsize}
|
||||||
|
class="media-focal-point-image"
|
||||||
|
src={previewSrc}
|
||||||
|
alt={shortName}
|
||||||
|
on:load="onImageLoad()"
|
||||||
|
/>
|
||||||
|
<div class="media-focal-point-backdrop"></div>
|
||||||
|
<div class="media-draggable-area"
|
||||||
|
style={draggableAreaStyle}
|
||||||
|
>
|
||||||
|
<!-- 52px == 32px icon width + 10px padding -->
|
||||||
|
<Draggable
|
||||||
|
draggableClass="media-draggable-area-inner"
|
||||||
|
indicatorClass="media-focal-point-indicator {imageLoaded ? '': 'hidden'}"
|
||||||
|
indicatorWidth={52}
|
||||||
|
indicatorHeight={52}
|
||||||
|
x={indicatorX}
|
||||||
|
y={indicatorY}
|
||||||
|
on:change="onDraggableChange(event)"
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
className="media-focal-point-indicator-svg"
|
||||||
|
href="#fa-crosshairs"
|
||||||
|
/>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-focal-point-inputs">
|
||||||
|
<div class="media-focal-point-input-pair">
|
||||||
|
<label for="media-focal-point-x-input-{realm}">
|
||||||
|
X coordinate
|
||||||
|
</label>
|
||||||
|
<input type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="-1"
|
||||||
|
max="1"
|
||||||
|
inputmode="decimal"
|
||||||
|
placeholder="0"
|
||||||
|
id="media-focal-point-x-input-{realm}"
|
||||||
|
bind:value="rawFocusX"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="media-focal-point-input-pair">
|
||||||
|
<label for="media-focal-point-y-input-{realm}">
|
||||||
|
Y coordinate
|
||||||
|
</label>
|
||||||
|
<input type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="-1"
|
||||||
|
max="1"
|
||||||
|
inputmode="decimal"
|
||||||
|
placeholder="0"
|
||||||
|
id="media-focal-point-y-input-{realm}"
|
||||||
|
bind:value="rawFocusY"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalDialog>
|
||||||
|
<style>
|
||||||
|
:global(.media-focal-point-dialog) {
|
||||||
|
max-width: calc(100%);
|
||||||
|
}
|
||||||
|
.media-focal-point-container {
|
||||||
|
height: calc(100% - 44px); /* 44px X button height */
|
||||||
|
width: calc(100vw - 40px);
|
||||||
|
padding-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.media-focal-point-image-container {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.media-focal-point-image {
|
||||||
|
object-fit: contain;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-focal-point-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (-webkit-backdrop-filter: blur(1px) saturate(1%)) or (backdrop-filter: blur(1px) saturate(1%)) {
|
||||||
|
.media-focal-point-backdrop {
|
||||||
|
-webkit-backdrop-filter: blur(2px) saturate(110%);
|
||||||
|
backdrop-filter: blur(2px) saturate(110%);
|
||||||
|
background-color: var(--focal-img-backdrop-filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports not ((-webkit-backdrop-filter: blur(1px) saturate(1%)) or (backdrop-filter: blur(1px) saturate(1%))) {
|
||||||
|
.media-focal-point-backdrop {
|
||||||
|
background-color: var(--focal-img-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-focal-point-inputs {
|
||||||
|
display: flex;
|
||||||
|
padding: 20px 40px;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-focal-point-input-pair {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-focal-point-input-pair input {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-draggable-area {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.media-focal-point-indicator) {
|
||||||
|
background: var(--focal-bg);
|
||||||
|
border-radius: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.media-draggable-area-inner) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.media-focal-point-indicator-svg) {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 10px;
|
||||||
|
fill: var(--focal-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.media-focal-point-inputs {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
.media-focal-point-input-pair {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
.media-focal-point-input-pair input {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import ModalDialog from './ModalDialog.html'
|
||||||
|
import { show } from '../helpers/showDialog'
|
||||||
|
import { close } from '../helpers/closeDialog'
|
||||||
|
import { oncreate as onCreateDialog } from '../helpers/onCreateDialog'
|
||||||
|
import { store } from '../../../_store/store'
|
||||||
|
import { get } from '../../../_utils/lodash-lite'
|
||||||
|
import { observe } from 'svelte-extras'
|
||||||
|
import debounce from 'lodash-es/debounce'
|
||||||
|
import { scheduleIdleTask } from '../../../_utils/scheduleIdleTask'
|
||||||
|
import { coordsToPercent, percentToCoords } from '../../../_utils/coordsToPercent'
|
||||||
|
import SvgIcon from '../../SvgIcon.html'
|
||||||
|
import { intrinsicScale } from '../../../_thirdparty/intrinsic-scale/intrinsicScale'
|
||||||
|
import { resize } from '../../../_utils/events'
|
||||||
|
import Draggable from '../../Draggable.html'
|
||||||
|
|
||||||
|
const parseAndValidateFloat = rawText => {
|
||||||
|
let float = parseFloat(rawText)
|
||||||
|
if (Number.isNaN(float)) {
|
||||||
|
float = 0
|
||||||
|
}
|
||||||
|
float = Math.min(1, float)
|
||||||
|
float = Math.max(-1, float)
|
||||||
|
float = Math.round(float * 100) / 100
|
||||||
|
return float
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
oncreate () {
|
||||||
|
onCreateDialog.call(this)
|
||||||
|
this.setupSyncFromStore()
|
||||||
|
this.setupSyncToStore()
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ModalDialog,
|
||||||
|
SvgIcon,
|
||||||
|
Draggable
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
rawFocusX: '0',
|
||||||
|
rawFocusY: '0',
|
||||||
|
containerWidth: 0,
|
||||||
|
containerHeight: 0,
|
||||||
|
imageLoaded: false
|
||||||
|
}),
|
||||||
|
store: () => store,
|
||||||
|
computed: {
|
||||||
|
media: ({ $currentInstance, $composeData, realm }) => (
|
||||||
|
get($composeData, [$currentInstance, realm, 'media'])
|
||||||
|
),
|
||||||
|
mediaItem: ({ media, index }) => get(media, [index]),
|
||||||
|
focusX: ({ mediaItem }) => get(mediaItem, ['focusX'], 0),
|
||||||
|
focusY: ({ mediaItem }) => get(mediaItem, ['focusY'], 0),
|
||||||
|
previewSrc: ({ mediaItem }) => mediaItem.data.preview_url,
|
||||||
|
nativeWidth: ({ mediaItem }) => get(mediaItem, ['data', 'meta', 'original', 'width'], 300),
|
||||||
|
nativeHeight: ({ mediaItem }) => get(mediaItem, ['data', 'meta', 'original', 'height'], 200),
|
||||||
|
shortName: ({ mediaItem }) => (
|
||||||
|
// sometimes we no longer have the file, e.g. in a delete and redraft situation,
|
||||||
|
// so fall back to the description if it was provided
|
||||||
|
get(mediaItem, ['file', 'name']) || get(mediaItem, ['description']) || 'media'
|
||||||
|
),
|
||||||
|
intrinsicsize: ({ mediaItem }) => {
|
||||||
|
let width = get(mediaItem, ['data', 'meta', 'original', 'width'])
|
||||||
|
let height = get(mediaItem, ['data', 'meta', 'original', 'height'])
|
||||||
|
if (width && height) {
|
||||||
|
return `${width} x ${height}`
|
||||||
|
}
|
||||||
|
return '' // pleroma does not give us original width/height
|
||||||
|
},
|
||||||
|
scale: ({ nativeWidth, nativeHeight, containerWidth, containerHeight }) => (
|
||||||
|
intrinsicScale(containerWidth, containerHeight, nativeWidth, nativeHeight)
|
||||||
|
),
|
||||||
|
scaleWidth: ({ scale }) => scale.width,
|
||||||
|
scaleHeight: ({ scale }) => scale.height,
|
||||||
|
scaleX: ({ scale }) => scale.x,
|
||||||
|
scaleY: ({ scale }) => scale.y,
|
||||||
|
indicatorX: ({ focusX }) => (coordsToPercent(focusX) / 100),
|
||||||
|
indicatorY: ({ focusY }) => ((100 - coordsToPercent(focusY)) / 100),
|
||||||
|
draggableAreaStyle: ({ scaleWidth, scaleHeight, scaleX, scaleY }) => (
|
||||||
|
`top: ${scaleY}px; left: ${scaleX}px; width: ${scaleWidth}px; height: ${scaleHeight}px;`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
observe,
|
||||||
|
show,
|
||||||
|
close,
|
||||||
|
setupSyncFromStore () {
|
||||||
|
this.observe('mediaItem', mediaItem => {
|
||||||
|
let { rawFocusX, rawFocusY } = this.get()
|
||||||
|
|
||||||
|
const syncFromStore = (rawKey, rawFocus, key) => {
|
||||||
|
let focus = get(mediaItem, [key], 0) || 0
|
||||||
|
let focusAsString = focus.toString()
|
||||||
|
if (focusAsString !== rawFocus) {
|
||||||
|
this.set({ [rawKey]: focusAsString })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncFromStore('rawFocusX', rawFocusX, 'focusX')
|
||||||
|
syncFromStore('rawFocusY', rawFocusY, 'focusY')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setupSyncToStore () {
|
||||||
|
const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
|
||||||
|
|
||||||
|
const observeAndSync = (rawKey, key) => {
|
||||||
|
this.observe(rawKey, rawFocus => {
|
||||||
|
let { realm, index, media } = this.get()
|
||||||
|
let rawFocusDecimal = parseAndValidateFloat(rawFocus)
|
||||||
|
if (media[index][key] !== rawFocusDecimal) {
|
||||||
|
media[index][key] = rawFocusDecimal
|
||||||
|
this.store.setComposeData(realm, { media })
|
||||||
|
saveStore()
|
||||||
|
}
|
||||||
|
}, { init: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
observeAndSync('rawFocusX', 'focusX')
|
||||||
|
observeAndSync('rawFocusY', 'focusY')
|
||||||
|
},
|
||||||
|
onDraggableChange ({ x, y }) {
|
||||||
|
const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
|
||||||
|
|
||||||
|
scheduleIdleTask(() => {
|
||||||
|
let focusX = percentToCoords(x * 100)
|
||||||
|
let focusY = percentToCoords(100 - (y * 100))
|
||||||
|
let { realm, index, media } = this.get()
|
||||||
|
media[index].focusX = parseAndValidateFloat(focusX)
|
||||||
|
media[index].focusY = parseAndValidateFloat(focusY)
|
||||||
|
this.store.setComposeData(realm, { media })
|
||||||
|
saveStore()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
measure () {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!this.refs.container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let rect = this.refs.container.getBoundingClientRect()
|
||||||
|
this.set({
|
||||||
|
containerWidth: rect.width,
|
||||||
|
containerHeight: rect.height
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onImageLoad () {
|
||||||
|
this.measure()
|
||||||
|
this.set({ imageLoaded: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
resize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -69,6 +69,8 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
import { get } from '../../../_utils/lodash-lite'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
type: ({ media }) => media.type,
|
type: ({ media }) => media.type,
|
||||||
|
@ -77,8 +79,9 @@
|
||||||
poster: ({ media }) => media.preview_url,
|
poster: ({ media }) => media.preview_url,
|
||||||
static_url: ({ media }) => media.static_url,
|
static_url: ({ media }) => media.static_url,
|
||||||
intrinsicsize: ({ media }) => {
|
intrinsicsize: ({ media }) => {
|
||||||
if (media.meta && media.meta.original && media.meta.original.width && media.meta.original.height) {
|
let width = get(media, ['meta', 'original', 'width'])
|
||||||
let { width, height } = media.meta.original
|
let height = get(media, ['meta', 'original', 'height'])
|
||||||
|
if (width && height) {
|
||||||
return `${width} x ${height}`
|
return `${width} x ${height}`
|
||||||
}
|
}
|
||||||
return '' // pleroma does not give us original width/height
|
return '' // pleroma does not give us original width/height
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import MediaFocalPointDialog from '../components/MediaFocalPointDialog.html'
|
||||||
|
import { showDialog } from './showDialog'
|
||||||
|
|
||||||
|
export default function showMediaFocalPointDialog (realm, index) {
|
||||||
|
return showDialog(MediaFocalPointDialog, {
|
||||||
|
label: 'Change preview dialog',
|
||||||
|
title: 'Change preview (focal point)',
|
||||||
|
realm,
|
||||||
|
index
|
||||||
|
})
|
||||||
|
}
|
21
src/routes/_thirdparty/intrinsic-scale/LICENSE
vendored
Normal file
21
src/routes/_thirdparty/intrinsic-scale/LICENSE
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) Federico Brigante <bfred-it@users.noreply.github.com> (twitter.com/bfred_it)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
21
src/routes/_thirdparty/intrinsic-scale/intrinsicScale.js
vendored
Normal file
21
src/routes/_thirdparty/intrinsic-scale/intrinsicScale.js
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// via https://github.com/bfred-it/intrinsic-scale/blob/3d058f79902653484092ad9a2f3e1d9a3d03f09e/index.js
|
||||||
|
|
||||||
|
export const intrinsicScale = (parentWidth, parentHeight, childWidth, childHeight) => {
|
||||||
|
const doRatio = childWidth / childHeight
|
||||||
|
const cRatio = parentWidth / parentHeight
|
||||||
|
let width = parentWidth
|
||||||
|
let height = parentHeight
|
||||||
|
|
||||||
|
if (doRatio > cRatio) {
|
||||||
|
height = width / doRatio
|
||||||
|
} else {
|
||||||
|
width = height * doRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x: (parentWidth - width) / 2,
|
||||||
|
y: (parentHeight - height) / 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,3 +13,7 @@ export const importIndexedDBGetAllShim = () => import(
|
||||||
export const importCustomElementsPolyfill = () => import(
|
export const importCustomElementsPolyfill = () => import(
|
||||||
/* webpackChunkName: '$polyfill$-@webcomponents/custom-elements' */ '@webcomponents/custom-elements'
|
/* webpackChunkName: '$polyfill$-@webcomponents/custom-elements' */ '@webcomponents/custom-elements'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const importPointerEventsPolyfill = () => import(
|
||||||
|
/* webpackChunkName: '$polyfill$-@wessberg/pointer-events' */ '@wessberg/pointer-events'
|
||||||
|
)
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export const coordsToPercent = coord => (1 + coord) / 2 * 100
|
export const coordsToPercent = coord => ((1 + coord) / 2) * 100
|
||||||
|
|
||||||
|
export const percentToCoords = percent => ((percent / 100) * 2) - 1
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { registerResizeListener, unregisterResizeListener } from './resize'
|
||||||
|
|
||||||
export function mouseover (node, callback) {
|
export function mouseover (node, callback) {
|
||||||
function onMouseEnter () {
|
function onMouseEnter () {
|
||||||
callback(true) // eslint-disable-line
|
callback(true) // eslint-disable-line
|
||||||
|
@ -71,3 +73,13 @@ export function applyFocusStylesToParent (node) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resize (node, callback) {
|
||||||
|
registerResizeListener(callback)
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy () {
|
||||||
|
unregisterResizeListener(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ import {
|
||||||
importCustomElementsPolyfill,
|
importCustomElementsPolyfill,
|
||||||
importIndexedDBGetAllShim,
|
importIndexedDBGetAllShim,
|
||||||
importIntersectionObserver,
|
importIntersectionObserver,
|
||||||
importRequestIdleCallback
|
importRequestIdleCallback,
|
||||||
|
importPointerEventsPolyfill
|
||||||
} from './asyncPolyfills'
|
} from './asyncPolyfills'
|
||||||
|
|
||||||
export function loadPolyfills () {
|
export function loadPolyfills () {
|
||||||
|
@ -10,6 +11,7 @@ export function loadPolyfills () {
|
||||||
typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
|
typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
|
||||||
typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(),
|
typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(),
|
||||||
!IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(),
|
!IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(),
|
||||||
typeof customElements === 'undefined' && importCustomElementsPolyfill()
|
typeof customElements === 'undefined' && importCustomElementsPolyfill(),
|
||||||
|
typeof PointerEvent === 'undefined' && importPointerEventsPolyfill()
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
18
src/routes/_utils/throttleRaf.js
Normal file
18
src/routes/_utils/throttleRaf.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// ensure callback is only executed once per raf
|
||||||
|
export const throttleRaf = () => {
|
||||||
|
let rafCallback
|
||||||
|
let rafQueued
|
||||||
|
|
||||||
|
return function throttledRaf (callback) {
|
||||||
|
rafCallback = callback
|
||||||
|
if (!rafQueued) {
|
||||||
|
rafQueued = true
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
let cb = rafCallback
|
||||||
|
rafCallback = null
|
||||||
|
rafQueued = false
|
||||||
|
cb()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -120,4 +120,9 @@
|
||||||
--length-indicator-color: #{$main-theme-color};
|
--length-indicator-color: #{$main-theme-color};
|
||||||
|
|
||||||
--audio-bg: #{rgba(30, 30, 30, 0.8)};
|
--audio-bg: #{rgba(30, 30, 30, 0.8)};
|
||||||
|
|
||||||
|
--focal-img-backdrop-filter: #{rgba($main-bg-color, 0.5)};
|
||||||
|
--focal-img-bg: #{rgba($main-bg-color, 0.3)};
|
||||||
|
--focal-bg: #{rgba($toast-bg, 0.8)};
|
||||||
|
--focal-color: #{$secondary-text-color};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import {
|
import {
|
||||||
composeInput, getNthDeleteMediaButton, getNthMedia, getNthMediaAltInput, homeNavButton, mediaButton,
|
composeInput,
|
||||||
settingsNavButton, sleep,
|
getNthDeleteMediaButton,
|
||||||
|
getNthMedia,
|
||||||
|
getNthMediaAltInput,
|
||||||
|
getNthMediaListItem,
|
||||||
|
homeNavButton,
|
||||||
|
mediaButton,
|
||||||
|
settingsNavButton,
|
||||||
|
sleep,
|
||||||
uploadKittenImage
|
uploadKittenImage
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
|
@ -13,21 +20,21 @@ test('inserts media', async t => {
|
||||||
await t
|
await t
|
||||||
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
||||||
await (uploadKittenImage(1)())
|
await (uploadKittenImage(1)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
await (uploadKittenImage(2)())
|
await (uploadKittenImage(2)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
||||||
await (uploadKittenImage(3)())
|
await (uploadKittenImage(3)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.expect(getNthMedia(3).getAttribute('alt')).eql('kitten3.jpg')
|
.expect(getNthMediaListItem(3).getAttribute('aria-label')).eql('kitten3.jpg')
|
||||||
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
||||||
await (uploadKittenImage(4)())
|
await (uploadKittenImage(4)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.expect(getNthMedia(3).getAttribute('alt')).eql('kitten3.jpg')
|
.expect(getNthMediaListItem(3).getAttribute('aria-label')).eql('kitten3.jpg')
|
||||||
.expect(getNthMedia(4).getAttribute('alt')).eql('kitten4.jpg')
|
.expect(getNthMediaListItem(4).getAttribute('aria-label')).eql('kitten4.jpg')
|
||||||
.expect(mediaButton.getAttribute('disabled')).eql('')
|
.expect(mediaButton.getAttribute('disabled')).eql('')
|
||||||
.click(getNthDeleteMediaButton(4))
|
.click(getNthDeleteMediaButton(4))
|
||||||
.click(getNthDeleteMediaButton(3))
|
.click(getNthDeleteMediaButton(3))
|
||||||
|
@ -41,10 +48,10 @@ test('removes media', async t => {
|
||||||
await t
|
await t
|
||||||
.expect(mediaButton.exists).ok()
|
.expect(mediaButton.exists).ok()
|
||||||
await (uploadKittenImage(1)())
|
await (uploadKittenImage(1)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
await (uploadKittenImage(2)())
|
await (uploadKittenImage(2)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.click(getNthDeleteMediaButton(2))
|
.click(getNthDeleteMediaButton(2))
|
||||||
.expect(getNthMedia(2).exists).notOk()
|
.expect(getNthMedia(2).exists).notOk()
|
||||||
.expect(getNthMedia(1).exists).ok()
|
.expect(getNthMedia(1).exists).ok()
|
||||||
|
@ -79,11 +86,11 @@ test('keeps media descriptions as media is removed', async t => {
|
||||||
.typeText(getNthMediaAltInput(2), 'kitten numero dos')
|
.typeText(getNthMediaAltInput(2), 'kitten numero dos')
|
||||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.click(getNthDeleteMediaButton(1))
|
.click(getNthDeleteMediaButton(1))
|
||||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero dos')
|
.expect(getNthMediaAltInput(1).value).eql('kitten numero dos')
|
||||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('keeps media in local storage', async t => {
|
test('keeps media in local storage', async t => {
|
||||||
|
@ -101,8 +108,8 @@ test('keeps media in local storage', async t => {
|
||||||
.expect(composeInput.value).eql('hello hello')
|
.expect(composeInput.value).eql('hello hello')
|
||||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
await sleep(1)
|
await sleep(1)
|
||||||
await t
|
await t
|
||||||
.click(settingsNavButton)
|
.click(settingsNavButton)
|
||||||
|
@ -110,12 +117,12 @@ test('keeps media in local storage', async t => {
|
||||||
.expect(composeInput.value).eql('hello hello')
|
.expect(composeInput.value).eql('hello hello')
|
||||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.navigateTo('/')
|
.navigateTo('/')
|
||||||
.expect(composeInput.value).eql('hello hello')
|
.expect(composeInput.value).eql('hello hello')
|
||||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import {
|
import {
|
||||||
composeButton, composeInput, getNthDeleteMediaButton, getNthMedia, getNthMediaAltInput, getNthStatusAndImage, getUrl,
|
composeButton,
|
||||||
|
composeInput,
|
||||||
|
getNthDeleteMediaButton,
|
||||||
|
getNthMedia,
|
||||||
|
getNthMediaAltInput,
|
||||||
|
getNthMediaListItem,
|
||||||
|
getNthStatusAndImage,
|
||||||
|
getUrl,
|
||||||
homeNavButton,
|
homeNavButton,
|
||||||
mediaButton, notificationsNavButton,
|
mediaButton,
|
||||||
|
notificationsNavButton,
|
||||||
uploadKittenImage
|
uploadKittenImage
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
|
@ -11,10 +19,10 @@ fixture`109-compose-media.js`
|
||||||
|
|
||||||
async function uploadTwoKittens (t) {
|
async function uploadTwoKittens (t) {
|
||||||
await (uploadKittenImage(1)())
|
await (uploadKittenImage(1)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
await (uploadKittenImage(2)())
|
await (uploadKittenImage(2)())
|
||||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
test('uploads alts for media', async t => {
|
test('uploads alts for media', async t => {
|
||||||
|
@ -25,10 +33,10 @@ test('uploads alts for media', async t => {
|
||||||
await t.typeText(getNthMediaAltInput(2), 'kitten 2')
|
await t.typeText(getNthMediaAltInput(2), 'kitten 2')
|
||||||
.typeText(getNthMediaAltInput(1), 'kitten 1')
|
.typeText(getNthMediaAltInput(1), 'kitten 1')
|
||||||
.click(composeButton)
|
.click(composeButton)
|
||||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('kitten 1')
|
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten 1')
|
||||||
.expect(getNthStatusAndImage(1, 0).getAttribute('title')).eql('kitten 1')
|
.expect(getNthStatusAndImage(1, 1).getAttribute('title')).eql('kitten 1')
|
||||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten 2')
|
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten 2')
|
||||||
.expect(getNthStatusAndImage(1, 1).getAttribute('title')).eql('kitten 2')
|
.expect(getNthStatusAndImage(1, 2).getAttribute('title')).eql('kitten 2')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('uploads alts when deleting and re-uploading media', async t => {
|
test('uploads alts when deleting and re-uploading media', async t => {
|
||||||
|
@ -41,10 +49,10 @@ test('uploads alts when deleting and re-uploading media', async t => {
|
||||||
.expect(getNthMedia(1).exists).notOk()
|
.expect(getNthMedia(1).exists).notOk()
|
||||||
await (uploadKittenImage(2)())
|
await (uploadKittenImage(2)())
|
||||||
await t.expect(getNthMediaAltInput(1).value).eql('')
|
await t.expect(getNthMediaAltInput(1).value).eql('')
|
||||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.typeText(getNthMediaAltInput(1), 'this will not be deleted')
|
.typeText(getNthMediaAltInput(1), 'this will not be deleted')
|
||||||
.click(composeButton)
|
.click(composeButton)
|
||||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('this will not be deleted')
|
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('this will not be deleted')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('uploads alts mixed with no-alts', async t => {
|
test('uploads alts mixed with no-alts', async t => {
|
||||||
|
@ -54,8 +62,8 @@ test('uploads alts mixed with no-alts', async t => {
|
||||||
await uploadTwoKittens(t)
|
await uploadTwoKittens(t)
|
||||||
await t.typeText(getNthMediaAltInput(2), 'kitten numero dos')
|
await t.typeText(getNthMediaAltInput(2), 'kitten numero dos')
|
||||||
.click(composeButton)
|
.click(composeButton)
|
||||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('')
|
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('')
|
||||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero dos')
|
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten numero dos')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('saves alts to local storage', async t => {
|
test('saves alts to local storage', async t => {
|
||||||
|
@ -69,13 +77,13 @@ test('saves alts to local storage', async t => {
|
||||||
.expect(getUrl()).contains('/notifications')
|
.expect(getUrl()).contains('/notifications')
|
||||||
.click(homeNavButton)
|
.click(homeNavButton)
|
||||||
.expect(getUrl()).eql('http://localhost:4002/')
|
.expect(getUrl()).eql('http://localhost:4002/')
|
||||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||||
.click(composeButton)
|
.click(composeButton)
|
||||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('kitten numero uno')
|
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero uno')
|
||||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero dos')
|
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten numero dos')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can post a status with empty content if there is media', async t => {
|
test('can post a status with empty content if there is media', async t => {
|
||||||
|
@ -87,5 +95,5 @@ test('can post a status with empty content if there is media', async t => {
|
||||||
await t
|
await t
|
||||||
.typeText(getNthMediaAltInput(1), 'just an image!')
|
.typeText(getNthMediaAltInput(1), 'just an image!')
|
||||||
await t.click(composeButton)
|
await t.click(composeButton)
|
||||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('just an image!')
|
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('just an image!')
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,12 +8,11 @@ import {
|
||||||
composeModalInput,
|
composeModalInput,
|
||||||
getNthStatusMediaImg,
|
getNthStatusMediaImg,
|
||||||
composeModalPostPrivacyButton,
|
composeModalPostPrivacyButton,
|
||||||
getComposeModalNthMediaImg,
|
|
||||||
getComposeModalNthMediaAltInput,
|
getComposeModalNthMediaAltInput,
|
||||||
getNthStatusSpoiler,
|
getNthStatusSpoiler,
|
||||||
composeModalContentWarningInput,
|
composeModalContentWarningInput,
|
||||||
dialogOptionsOption,
|
dialogOptionsOption,
|
||||||
getNthReplyButton, getNthComposeReplyInput, getNthComposeReplyButton, getUrl, sleep
|
getNthReplyButton, getNthComposeReplyInput, getNthComposeReplyButton, getUrl, sleep, getComposeModalNthMediaListItem
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions'
|
import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions'
|
||||||
|
|
||||||
|
@ -48,7 +47,7 @@ test('image with empty text delete and redraft', async t => {
|
||||||
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
|
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
|
||||||
.expect(composeModalInput.value).eql('')
|
.expect(composeModalInput.value).eql('')
|
||||||
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
|
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
|
||||||
.expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('what a kitteh')
|
.expect(getComposeModalNthMediaListItem(1).getAttribute('aria-label')).eql('what a kitteh')
|
||||||
.expect(getComposeModalNthMediaAltInput(1).value).eql('what a kitteh')
|
.expect(getComposeModalNthMediaAltInput(1).value).eql('what a kitteh')
|
||||||
.typeText(composeModalInput, 'I love this kitteh', { replace: true, paste: true })
|
.typeText(composeModalInput, 'I love this kitteh', { replace: true, paste: true })
|
||||||
.click(composeModalComposeButton)
|
.click(composeModalComposeButton)
|
||||||
|
@ -68,7 +67,7 @@ test('image with no alt delete and redraft', async t => {
|
||||||
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
|
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
|
||||||
.expect(composeModalInput.value).eql('')
|
.expect(composeModalInput.value).eql('')
|
||||||
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
|
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
|
||||||
.expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('')
|
.expect(getComposeModalNthMediaListItem(1).getAttribute('aria-label')).eql('media')
|
||||||
.expect(getComposeModalNthMediaAltInput(1).value).eql('')
|
.expect(getComposeModalNthMediaAltInput(1).value).eql('')
|
||||||
.typeText(composeModalInput, 'oops forgot an alt', { replace: true, paste: true })
|
.typeText(composeModalInput, 'oops forgot an alt', { replace: true, paste: true })
|
||||||
.typeText(getComposeModalNthMediaAltInput(1), 'lovely kitteh', { replace: true, paste: true })
|
.typeText(getComposeModalNthMediaAltInput(1), 'lovely kitteh', { replace: true, paste: true })
|
||||||
|
|
59
tests/spec/130-focal-point.js
Normal file
59
tests/spec/130-focal-point.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import {
|
||||||
|
sleep,
|
||||||
|
composeInput,
|
||||||
|
mediaButton,
|
||||||
|
uploadKittenImage,
|
||||||
|
getNthMediaAltInput,
|
||||||
|
getNthMediaFocalPointButton,
|
||||||
|
modalDialog,
|
||||||
|
focalPointXInput,
|
||||||
|
closeDialogButton,
|
||||||
|
composeButton,
|
||||||
|
focalPointYInput,
|
||||||
|
getNthStatusContent,
|
||||||
|
getNthStatusAndImage
|
||||||
|
} from '../utils'
|
||||||
|
import { loginAsFoobar } from '../roles'
|
||||||
|
|
||||||
|
fixture`130-focal-point.js`
|
||||||
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
|
test('Can set a focal point', async t => {
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await t
|
||||||
|
.typeText(composeInput, 'here is a focal point')
|
||||||
|
.click(mediaButton)
|
||||||
|
await (uploadKittenImage(1)())
|
||||||
|
await (uploadKittenImage(2)())
|
||||||
|
await (uploadKittenImage(3)())
|
||||||
|
await t
|
||||||
|
.typeText(getNthMediaAltInput(1), 'kitten 1')
|
||||||
|
.typeText(getNthMediaAltInput(2), 'kitten 2')
|
||||||
|
.click(getNthMediaFocalPointButton(2))
|
||||||
|
.expect(modalDialog.hasAttribute('aria-hidden')).notOk({ timeout: 30000 })
|
||||||
|
.typeText(focalPointXInput, '0.5')
|
||||||
|
await sleep(1000)
|
||||||
|
await t
|
||||||
|
.click(closeDialogButton)
|
||||||
|
.expect(modalDialog.exists).notOk()
|
||||||
|
await sleep(1000)
|
||||||
|
await t
|
||||||
|
.click(getNthMediaFocalPointButton(3))
|
||||||
|
.expect(modalDialog.hasAttribute('aria-hidden')).notOk({ timeout: 30000 })
|
||||||
|
.typeText(focalPointXInput, '-0.25')
|
||||||
|
.typeText(focalPointYInput, '1')
|
||||||
|
await sleep(1000)
|
||||||
|
await t
|
||||||
|
.click(closeDialogButton)
|
||||||
|
.expect(modalDialog.exists).notOk()
|
||||||
|
await sleep(1000)
|
||||||
|
await t
|
||||||
|
.click(composeButton)
|
||||||
|
.expect(getNthStatusContent(1).innerText).contains('here is a focal point', { timeout: 30000 })
|
||||||
|
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten 1')
|
||||||
|
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten 2')
|
||||||
|
.expect(getNthStatusAndImage(1, 3).getAttribute('alt')).eql('')
|
||||||
|
.expect(getNthStatusAndImage(1, 1).getAttribute('style')).eql('object-position: 50% 50%;')
|
||||||
|
.expect(getNthStatusAndImage(1, 2).getAttribute('style')).eql('object-position: 75% 50%;')
|
||||||
|
.expect(getNthStatusAndImage(1, 3).getAttribute('style')).eql('object-position: 62.5% 0%;')
|
||||||
|
})
|
|
@ -85,12 +85,15 @@ export const instanceSettingNotificationMentions = $('#instance-option-notificat
|
||||||
|
|
||||||
export const notificationBadge = $('#main-nav li:nth-child(2) .nav-link-badge')
|
export const notificationBadge = $('#main-nav li:nth-child(2) .nav-link-badge')
|
||||||
|
|
||||||
|
export const focalPointXInput = $('.media-focal-point-inputs *:nth-child(1) input')
|
||||||
|
export const focalPointYInput = $('.media-focal-point-inputs *:nth-child(2) input')
|
||||||
|
|
||||||
export function getComposeModalNthMediaAltInput (n) {
|
export function getComposeModalNthMediaAltInput (n) {
|
||||||
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt textarea`)
|
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt textarea`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComposeModalNthMediaImg (n) {
|
export function getComposeModalNthMediaListItem (n) {
|
||||||
return $(`.modal-dialog .compose-media:nth-child(${n}) img`)
|
return $(`.modal-dialog .compose-media:nth-child(${n})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const favoritesCountElement = $('.status-favs').addCustomDOMProperties({
|
export const favoritesCountElement = $('.status-favs').addCustomDOMProperties({
|
||||||
|
@ -278,6 +281,10 @@ export function getNthSearchResult (n) {
|
||||||
return $(`.search-result:nth-child(${n}) a`)
|
return $(`.search-result:nth-child(${n}) a`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNthMediaListItem (n) {
|
||||||
|
return $(`.compose-media:nth-child(${n})`)
|
||||||
|
}
|
||||||
|
|
||||||
export function getNthMedia (n) {
|
export function getNthMedia (n) {
|
||||||
return $(`.compose-media:nth-child(${n}) img`)
|
return $(`.compose-media:nth-child(${n}) img`)
|
||||||
}
|
}
|
||||||
|
@ -286,6 +293,10 @@ export function getNthDeleteMediaButton (n) {
|
||||||
return $(`.compose-media:nth-child(${n}) .compose-media-delete-button`)
|
return $(`.compose-media:nth-child(${n}) .compose-media-delete-button`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNthMediaFocalPointButton (n) {
|
||||||
|
return $(`.compose-media:nth-child(${n}) .compose-media-focal-button`)
|
||||||
|
}
|
||||||
|
|
||||||
export function getAriaSetSize () {
|
export function getAriaSetSize () {
|
||||||
return getNthStatus(1 + 0).getAttribute('aria-setsize')
|
return getNthStatus(1 + 0).getAttribute('aria-setsize')
|
||||||
}
|
}
|
||||||
|
@ -331,7 +342,7 @@ export function getNthStatusHeader (n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNthStatusAndImage (nStatus, nImage) {
|
export function getNthStatusAndImage (nStatus, nImage) {
|
||||||
return $(`${getNthStatusSelector(nStatus)} .status-media .show-image-button:nth-child(${nImage + 1}) img`)
|
return $(`${getNthStatusSelector(nStatus)} .status-media .show-image-button:nth-child(${nImage}) img`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFirstVisibleStatus () {
|
export function getFirstVisibleStatus () {
|
||||||
|
|
|
@ -352,6 +352,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.2.4.tgz#7074543155396114617722724d6f6cb7b3800a14"
|
resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.2.4.tgz#7074543155396114617722724d6f6cb7b3800a14"
|
||||||
integrity sha512-WiTlgz6/kuwajYIcgyq64rSlCtb2AvbxwwrExP3wr6rKbJ72I3hi/sb4KdGUumfC+isDn2F0btZGk4MnWpyO1Q==
|
integrity sha512-WiTlgz6/kuwajYIcgyq64rSlCtb2AvbxwwrExP3wr6rKbJ72I3hi/sb4KdGUumfC+isDn2F0btZGk4MnWpyO1Q==
|
||||||
|
|
||||||
|
"@wessberg/pointer-events@^1.0.9":
|
||||||
|
version "1.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@wessberg/pointer-events/-/pointer-events-1.0.9.tgz#9591f2326e3592cdfa52d630217b97ecf7c5d817"
|
||||||
|
integrity sha512-fTOBNzakyi3wBXMmQ1mpAibU3lCbXVWSwgM9nsjtlfbJ5MH1yvteCZgZF+TDQWNtTPg8JWuQUjBF7+8cKoj92A==
|
||||||
|
|
||||||
"@xtuc/ieee754@^1.2.0":
|
"@xtuc/ieee754@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||||
|
|
Loading…
Reference in a new issue