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' : ''}"
|
<LengthGauge style="grid-area: gauge; margin: 0 0 5px 5px;"
|
||||||
style="transform: scaleX({lengthAsFractionDeferred});"
|
{length}
|
||||||
aria-hidden="true"
|
{overLimit}
|
||||||
></div>
|
max={$maxStatusChars}
|
||||||
<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>
|
|
||||||
<script>
|
<script>
|
||||||
import { mark, stop } from '../../_utils/marks'
|
import LengthGauge from '../LengthGauge.html'
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
|
||||||
import { observe } from 'svelte-extras'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
components: {
|
||||||
const { lengthAsFraction } = this.get()
|
LengthGauge
|
||||||
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 })
|
|
||||||
},
|
},
|
||||||
data: () => ({
|
store: () => store
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,56 +1,17 @@
|
||||||
<span class="compose-box-length {overLimit ? 'over-char-limit' : ''}"
|
<LengthIndicator
|
||||||
aria-label={lengthLabel}>
|
{length}
|
||||||
{lengthToDisplayDeferred}
|
{overLimit}
|
||||||
</span>
|
max={$maxStatusChars}
|
||||||
<style>
|
style="grid-area: length; justify-self: right; align-self: center;"
|
||||||
.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>
|
|
||||||
<script>
|
<script>
|
||||||
import { mark, stop } from '../../_utils/marks'
|
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
import LengthIndicator from '../LengthIndicator.html'
|
||||||
import { observe } from 'svelte-extras'
|
|
||||||
|
|
||||||
export default {
|
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,
|
store: () => store,
|
||||||
computed: {
|
components: {
|
||||||
lengthToDisplay: ({ length, $maxStatusChars }) => $maxStatusChars - length,
|
LengthIndicator
|
||||||
lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => {
|
|
||||||
if (overLimit) {
|
|
||||||
return `${lengthToDisplayDeferred} characters over limit`
|
|
||||||
} else {
|
|
||||||
return `${lengthToDisplayDeferred} characters remaining`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
observe
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
ref:textarea
|
ref:textarea
|
||||||
bind:value=rawText
|
bind:value=rawText
|
||||||
maxlength="420"
|
|
||||||
></textarea>
|
></textarea>
|
||||||
<label for="compose-media-input-{uuid}" class="sr-only">
|
<label for="compose-media-input-{uuid}" class="sr-only">
|
||||||
Describe for the visually impaired (image, video) or auditorily impaired (audio, video)
|
Describe for the visually impaired (image, video) or auditorily impaired (audio, video)
|
||||||
|
|
|
@ -5,15 +5,26 @@
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
ref:textarea
|
ref:textarea
|
||||||
bind:value=rawText
|
bind:value=rawText
|
||||||
maxlength="420"
|
|
||||||
></textarea>
|
></textarea>
|
||||||
<label for="the-media-alt-input-{realm}-{index}" class="sr-only">
|
<label for="the-media-alt-input-{realm}-{index}" class="sr-only">
|
||||||
Describe for the visually impaired
|
Describe for the visually impaired
|
||||||
</label>
|
</label>
|
||||||
|
<LengthGauge
|
||||||
|
{length}
|
||||||
|
{overLimit}
|
||||||
|
max={mediaAltCharLimit}
|
||||||
|
/>
|
||||||
|
<LengthIndicator
|
||||||
|
{length}
|
||||||
|
{overLimit}
|
||||||
|
max={mediaAltCharLimit}
|
||||||
|
style="width: 100%; text-align: right;"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
.media-alt-editor {
|
.media-alt-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.media-alt-input {
|
.media-alt-input {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
@ -42,6 +53,10 @@
|
||||||
import { throttleTimer } from '../../../_utils/throttleTimer'
|
import { throttleTimer } from '../../../_utils/throttleTimer'
|
||||||
import { get } from '../../../_utils/lodash-lite'
|
import { get } from '../../../_utils/lodash-lite'
|
||||||
import { store } from '../../../_store/store'
|
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)
|
const updateRawTextInStore = throttleTimer(requestPostAnimationFrame)
|
||||||
|
|
||||||
|
@ -56,8 +71,13 @@
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
rawText: ''
|
rawText: '',
|
||||||
|
mediaAltCharLimit: MEDIA_ALT_CHAR_LIMIT
|
||||||
}),
|
}),
|
||||||
|
computed: {
|
||||||
|
length: ({ rawText }) => length(rawText || ''),
|
||||||
|
overLimit: ({ mediaAltCharLimit, length }) => length > mediaAltCharLimit
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
observe,
|
observe,
|
||||||
setupSyncFromStore () {
|
setupSyncFromStore () {
|
||||||
|
@ -99,6 +119,10 @@
|
||||||
measure () {
|
measure () {
|
||||||
autosize.update(this.refs.textarea)
|
autosize.update(this.refs.textarea)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LengthGauge,
|
||||||
|
LengthIndicator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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,' +
|
'image/gif,video/webm,video/mp4,video/quicktime,' +
|
||||||
// some instances allow audio uploads
|
// some instances allow audio uploads
|
||||||
'audio/mpeg,audio/mp4,audio/vnd.wav,audio/wav,audio/x-wav,audio/x-wave,audio/ogg'
|
'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 composeInput = $('.compose-box-input')
|
||||||
export const composeContentWarning = $('.content-warning-input')
|
export const composeContentWarning = $('.content-warning-input')
|
||||||
export const composeButton = $('.compose-box-button')
|
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 emojiButton = $('.compose-box-toolbar button:first-child')
|
||||||
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
|
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
|
||||||
export const pollButton = $('.compose-box-toolbar button:nth-child(3)')
|
export const pollButton = $('.compose-box-toolbar button:nth-child(3)')
|
||||||
|
|
Loading…
Reference in a new issue