feat: add length gauge for media alt text editor (#1431)
* feat: add length gauge for media alt text editor * fix test
This commit is contained in:
parent
7f9195c2af
commit
59d26f1a09
59
src/routes/_components/LengthGauge.html
Normal file
59
src/routes/_components/LengthGauge.html
Normal file
|
@ -0,0 +1,59 @@
|
|||
<div class="length-gauge {shouldAnimate ? 'should-animate' : ''} {overLimit ? 'over-char-limit' : ''}"
|
||||
style={computedStyle}
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<style>
|
||||
.length-gauge {
|
||||
height: 2px;
|
||||
background: var(--main-theme-color);
|
||||
transform-origin: left;
|
||||
}
|
||||
.length-gauge.should-animate {
|
||||
transition: transform 0.2s linear;
|
||||
}
|
||||
.length-gauge.over-char-limit {
|
||||
background: var(--warning-color);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
import { store } from '../_store/store'
|
||||
import { observe } from 'svelte-extras'
|
||||
import { throttleTimer } from '../_utils/throttleTimer'
|
||||
|
||||
const updateDisplayedLength = process.browser && throttleTimer(requestAnimationFrame)
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
const { lengthAsFraction } = this.get()
|
||||
this.set({ lengthAsFractionDeferred: lengthAsFraction })
|
||||
// perf improvement for keyboard input latency
|
||||
this.observe('lengthAsFraction', () => {
|
||||
updateDisplayedLength(() => {
|
||||
mark('set lengthAsFractionDeferred')
|
||||
const { lengthAsFraction } = this.get()
|
||||
this.set({ lengthAsFractionDeferred: lengthAsFraction })
|
||||
stop('set lengthAsFractionDeferred')
|
||||
requestAnimationFrame(() => this.set({ shouldAnimate: true }))
|
||||
})
|
||||
}, { init: false })
|
||||
},
|
||||
data: () => ({
|
||||
shouldAnimate: false,
|
||||
lengthAsFractionDeferred: 0,
|
||||
style: ''
|
||||
}),
|
||||
store: () => store,
|
||||
computed: {
|
||||
lengthAsFraction: ({ length, max }) => {
|
||||
// We don't need to update the gauge for every decimal point, so round it to the nearest 0.02
|
||||
const int = Math.round(Math.min(max, length) / max * 100)
|
||||
return (int - (int % 2)) / 100
|
||||
},
|
||||
computedStyle: ({ style, lengthAsFractionDeferred }) => `transform: scaleX(${lengthAsFractionDeferred}); ${style}`
|
||||
},
|
||||
methods: {
|
||||
observe
|
||||
}
|
||||
}
|
||||
</script>
|
56
src/routes/_components/LengthIndicator.html
Normal file
56
src/routes/_components/LengthIndicator.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
<span class="length-indicator {overLimit ? 'over-char-limit' : ''}"
|
||||
aria-label={lengthLabel}
|
||||
{style}
|
||||
>{lengthToDisplayDeferred}</span>
|
||||
<style>
|
||||
.length-indicator {
|
||||
color: var(--length-indicator-color);
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.length-indicator.over-char-limit {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
import { store } from '../_store/store'
|
||||
import { observe } from 'svelte-extras'
|
||||
import { throttleTimer } from '../_utils/throttleTimer'
|
||||
|
||||
const updateDisplayedLength = process.browser && throttleTimer(requestAnimationFrame)
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
const { lengthToDisplay } = this.get()
|
||||
this.set({ lengthToDisplayDeferred: lengthToDisplay })
|
||||
// perf improvement for keyboard input latency
|
||||
this.observe('lengthToDisplay', () => {
|
||||
updateDisplayedLength(() => {
|
||||
mark('set lengthToDisplayDeferred')
|
||||
const { lengthToDisplay } = this.get()
|
||||
this.set({ lengthToDisplayDeferred: lengthToDisplay })
|
||||
stop('set lengthToDisplayDeferred')
|
||||
})
|
||||
}, { init: false })
|
||||
},
|
||||
data: () => ({
|
||||
lengthToDisplayDeferred: 0,
|
||||
style: ''
|
||||
}),
|
||||
store: () => store,
|
||||
computed: {
|
||||
lengthToDisplay: ({ length, max }) => max - length,
|
||||
lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => {
|
||||
if (overLimit) {
|
||||
return `${lengthToDisplayDeferred} characters over limit`
|
||||
} else {
|
||||
return `${lengthToDisplayDeferred} characters remaining`
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
observe
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,57 +1,16 @@
|
|||
<div class="compose-box-length-gauge {shouldAnimate ? 'should-animate' : ''} {overLimit ? 'over-char-limit' : ''}"
|
||||
style="transform: scaleX({lengthAsFractionDeferred});"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<style>
|
||||
.compose-box-length-gauge {
|
||||
grid-area: gauge;
|
||||
margin: 0 0 5px 5px;
|
||||
height: 2px;
|
||||
background: var(--main-theme-color);
|
||||
transform-origin: left;
|
||||
}
|
||||
.compose-box-length-gauge.should-animate {
|
||||
transition: transform 0.2s linear;
|
||||
}
|
||||
.compose-box-length-gauge.over-char-limit {
|
||||
background: var(--warning-color);
|
||||
}
|
||||
</style>
|
||||
<LengthGauge style="grid-area: gauge; margin: 0 0 5px 5px;"
|
||||
{length}
|
||||
{overLimit}
|
||||
max={$maxStatusChars}
|
||||
/>
|
||||
<script>
|
||||
import { mark, stop } from '../../_utils/marks'
|
||||
import LengthGauge from '../LengthGauge.html'
|
||||
import { store } from '../../_store/store'
|
||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||
import { observe } from 'svelte-extras'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
const { lengthAsFraction } = this.get()
|
||||
this.set({ lengthAsFractionDeferred: lengthAsFraction })
|
||||
// perf improvement for keyboard input latency
|
||||
this.observe('lengthAsFraction', () => {
|
||||
scheduleIdleTask(() => {
|
||||
mark('set lengthAsFractionDeferred')
|
||||
const { lengthAsFraction } = this.get()
|
||||
this.set({ lengthAsFractionDeferred: lengthAsFraction })
|
||||
stop('set lengthAsFractionDeferred')
|
||||
requestAnimationFrame(() => this.set({ shouldAnimate: true }))
|
||||
})
|
||||
}, { init: false })
|
||||
components: {
|
||||
LengthGauge
|
||||
},
|
||||
data: () => ({
|
||||
shouldAnimate: false,
|
||||
lengthAsFractionDeferred: 0
|
||||
}),
|
||||
store: () => store,
|
||||
computed: {
|
||||
lengthAsFraction: ({ length, $maxStatusChars }) => {
|
||||
// We don't need to update the gauge for every decimal point, so round it to the nearest 0.02
|
||||
const int = Math.round(Math.min($maxStatusChars, length) / $maxStatusChars * 100)
|
||||
return (int - (int % 2)) / 100
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
observe
|
||||
}
|
||||
store: () => store
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -1,56 +1,17 @@
|
|||
<span class="compose-box-length {overLimit ? 'over-char-limit' : ''}"
|
||||
aria-label={lengthLabel}>
|
||||
{lengthToDisplayDeferred}
|
||||
</span>
|
||||
<style>
|
||||
.compose-box-length {
|
||||
grid-area: length;
|
||||
justify-self: right;
|
||||
color: var(--length-indicator-color);
|
||||
font-size: 1.3em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.compose-box-length.over-char-limit {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
</style>
|
||||
<LengthIndicator
|
||||
{length}
|
||||
{overLimit}
|
||||
max={$maxStatusChars}
|
||||
style="grid-area: length; justify-self: right; align-self: center;"
|
||||
/>
|
||||
<script>
|
||||
import { mark, stop } from '../../_utils/marks'
|
||||
import { store } from '../../_store/store'
|
||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||
import { observe } from 'svelte-extras'
|
||||
import LengthIndicator from '../LengthIndicator.html'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
const { lengthToDisplay } = this.get()
|
||||
this.set({ lengthToDisplayDeferred: lengthToDisplay })
|
||||
// perf improvement for keyboard input latency
|
||||
this.observe('lengthToDisplay', () => {
|
||||
scheduleIdleTask(() => {
|
||||
mark('set lengthToDisplayDeferred')
|
||||
const { lengthToDisplay } = this.get()
|
||||
this.set({ lengthToDisplayDeferred: lengthToDisplay })
|
||||
stop('set lengthToDisplayDeferred')
|
||||
})
|
||||
}, { init: false })
|
||||
},
|
||||
data: () => ({
|
||||
lengthToDisplayDeferred: 0
|
||||
}),
|
||||
store: () => store,
|
||||
computed: {
|
||||
lengthToDisplay: ({ length, $maxStatusChars }) => $maxStatusChars - length,
|
||||
lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => {
|
||||
if (overLimit) {
|
||||
return `${lengthToDisplayDeferred} characters over limit`
|
||||
} else {
|
||||
return `${lengthToDisplayDeferred} characters remaining`
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
observe
|
||||
components: {
|
||||
LengthIndicator
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
placeholder="Description"
|
||||
ref:textarea
|
||||
bind:value=rawText
|
||||
maxlength="420"
|
||||
></textarea>
|
||||
<label for="compose-media-input-{uuid}" class="sr-only">
|
||||
Describe for the visually impaired (image, video) or auditorily impaired (audio, video)
|
||||
|
|
|
@ -5,15 +5,26 @@
|
|||
placeholder="Description"
|
||||
ref:textarea
|
||||
bind:value=rawText
|
||||
maxlength="420"
|
||||
></textarea>
|
||||
<label for="the-media-alt-input-{realm}-{index}" class="sr-only">
|
||||
Describe for the visually impaired
|
||||
</label>
|
||||
<LengthGauge
|
||||
{length}
|
||||
{overLimit}
|
||||
max={mediaAltCharLimit}
|
||||
/>
|
||||
<LengthIndicator
|
||||
{length}
|
||||
{overLimit}
|
||||
max={mediaAltCharLimit}
|
||||
style="width: 100%; text-align: right;"
|
||||
/>
|
||||
</div>
|
||||
<style>
|
||||
.media-alt-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.media-alt-input {
|
||||
padding: 5px;
|
||||
|
@ -42,6 +53,10 @@
|
|||
import { throttleTimer } from '../../../_utils/throttleTimer'
|
||||
import { get } from '../../../_utils/lodash-lite'
|
||||
import { store } from '../../../_store/store'
|
||||
import { MEDIA_ALT_CHAR_LIMIT } from '../../../_static/media'
|
||||
import LengthGauge from '../../LengthGauge.html'
|
||||
import LengthIndicator from '../../LengthIndicator.html'
|
||||
import { length } from 'stringz'
|
||||
|
||||
const updateRawTextInStore = throttleTimer(requestPostAnimationFrame)
|
||||
|
||||
|
@ -56,8 +71,13 @@
|
|||
},
|
||||
store: () => store,
|
||||
data: () => ({
|
||||
rawText: ''
|
||||
rawText: '',
|
||||
mediaAltCharLimit: MEDIA_ALT_CHAR_LIMIT
|
||||
}),
|
||||
computed: {
|
||||
length: ({ rawText }) => length(rawText || ''),
|
||||
overLimit: ({ mediaAltCharLimit, length }) => length > mediaAltCharLimit
|
||||
},
|
||||
methods: {
|
||||
observe,
|
||||
setupSyncFromStore () {
|
||||
|
@ -99,6 +119,10 @@
|
|||
measure () {
|
||||
autosize.update(this.refs.textarea)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
LengthGauge,
|
||||
LengthIndicator
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -8,3 +8,5 @@ export const mediaAccept = '.jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,.mov,image/jpeg
|
|||
'image/gif,video/webm,video/mp4,video/quicktime,' +
|
||||
// some instances allow audio uploads
|
||||
'audio/mpeg,audio/mp4,audio/vnd.wav,audio/wav,audio/x-wav,audio/x-wave,audio/ogg'
|
||||
|
||||
export const MEDIA_ALT_CHAR_LIMIT = 420
|
||||
|
|
|
@ -19,7 +19,7 @@ export const formError = $('.form-error-user-error')
|
|||
export const composeInput = $('.compose-box-input')
|
||||
export const composeContentWarning = $('.content-warning-input')
|
||||
export const composeButton = $('.compose-box-button')
|
||||
export const composeLengthIndicator = $('.compose-box-length')
|
||||
export const composeLengthIndicator = $('.length-indicator')
|
||||
export const emojiButton = $('.compose-box-toolbar button:first-child')
|
||||
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
|
||||
export const pollButton = $('.compose-box-toolbar button:nth-child(3)')
|
||||
|
|
Loading…
Reference in a new issue