fix: make autosuggest list appear over modal dialog (#1649)
fixes #1645
This commit is contained in:
parent
4221ce1c72
commit
fec0c282c9
|
@ -1,125 +1,120 @@
|
||||||
<div class="compose-autosuggest {shown ? '' : 'not-shown'} {realm === 'dialog' ? 'is-dialog' : ''}">
|
<div class="compose-autosuggest-anchor-point" ref:anchor></div>
|
||||||
<ComposeAutosuggestionList
|
|
||||||
items={autosuggestSearchResults}
|
|
||||||
on:click="onClick(event)"
|
|
||||||
type={autosuggestType}
|
|
||||||
selected={autosuggestSelected}
|
|
||||||
{realm}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<style>
|
<style>
|
||||||
.compose-autosuggest {
|
.compose-autosuggest-anchor-point {
|
||||||
grid-area: autosuggest;
|
grid-area: autosuggest;
|
||||||
position: absolute;
|
width: 100%;
|
||||||
left: 5px;
|
height: 0;
|
||||||
top: 0;
|
|
||||||
z-index: 7000;
|
|
||||||
}
|
}
|
||||||
.compose-autosuggest.is-dialog {
|
|
||||||
z-index: 11000;
|
|
||||||
}
|
|
||||||
.compose-autosuggest.not-shown {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
.compose-autosuggest {
|
|
||||||
min-width: 400px;
|
|
||||||
max-width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 479px) {
|
|
||||||
.compose-autosuggest {
|
|
||||||
/* on mobile, move it to the left and make it fill the viewport width */
|
|
||||||
transform: translateX(-58px); /* avatar size 48px + 10px padding */
|
|
||||||
width: calc(100vw - 20px);
|
|
||||||
}
|
|
||||||
.compose-autosuggest.is-dialog {
|
|
||||||
width: calc(100vw - 40px); /* extra padding when within the dialog */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 240px) {
|
|
||||||
.compose-autosuggest {
|
|
||||||
width: calc(100vw - 10px);
|
|
||||||
transform: translateX(-29px); /* avatar size 24px + 5px padding */
|
|
||||||
}
|
|
||||||
.compose-autosuggest.is-dialog {
|
|
||||||
transform: translateX(-34px); /* avatar size 24px + 10px padding */
|
|
||||||
width: calc(100vw - 20px); /* extra padding when within the dialog */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../../_store/store'
|
import ComposeAutosuggestContainer from './ComposeAutosuggestContainer.html'
|
||||||
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
|
import { requestPostAnimationFrame } from '../../_utils/requestPostAnimationFrame'
|
||||||
import { get } from '../../_utils/lodash-lite'
|
|
||||||
import { selectAutosuggestItem } from '../../_actions/autosuggest'
|
|
||||||
import { observe } from 'svelte-extras'
|
import { observe } from 'svelte-extras'
|
||||||
import { once } from '../../_utils/once'
|
import { throttleTimer } from '../../_utils/throttleTimer'
|
||||||
|
import { on } from '../../_utils/eventBus'
|
||||||
|
import { store } from '../../_store/store'
|
||||||
|
import { getScrollContainer } from '../../_utils/scrollContainer'
|
||||||
|
import { get } from '../../_utils/lodash-lite'
|
||||||
|
import { registerResizeListener, unregisterResizeListener } from '../../_utils/resize'
|
||||||
|
|
||||||
|
const updatePosition = process.browser && throttleTimer(requestAnimationFrame)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
this._promiseChain = Promise.resolve()
|
const { dialogId } = this.get()
|
||||||
this.observe('shouldBeShown', (shouldBeShown) => {
|
this.onResize = () => updatePosition(() => this.doResize())
|
||||||
// TODO: hack so that when the user clicks the button, and the textarea blurs,
|
let setupDone = false
|
||||||
// we don't immediately hide the dropdown which would cause the click to get lost
|
if (this.get().realm === 'dialog') {
|
||||||
this._promiseChain = this._promiseChain.then(() => {
|
// wait for dialog to render first
|
||||||
if (!shouldBeShown) {
|
on('dialogDidRender', this, id => {
|
||||||
return Promise.race([
|
if (id === dialogId && !setupDone) {
|
||||||
new Promise(resolve => setTimeout(resolve, 200)),
|
setupDone = true
|
||||||
new Promise(resolve => this.once('autosuggestItemSelected', resolve))
|
this.setup()
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}).then(() => {
|
|
||||||
this.set({ shown: shouldBeShown })
|
|
||||||
this.store.setForCurrentAutosuggest({ autosuggestSelecting: false })
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
setupDone = true
|
||||||
|
this.setup()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ondestroy () {
|
||||||
|
if (this._autosuggest) {
|
||||||
|
this._autosuggest.destroy()
|
||||||
|
this._autosuggest = null
|
||||||
|
}
|
||||||
|
if (this._element) {
|
||||||
|
this._element.remove()
|
||||||
|
}
|
||||||
|
unregisterResizeListener(this.onResize)
|
||||||
|
document.removeEventListener('scroll', this.onResize)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
observe,
|
observe,
|
||||||
once,
|
setup () {
|
||||||
onClick (item) {
|
const { realm, text, dialogId } = this.get()
|
||||||
/* autosuggestSelecting prevents a flash of searched content */
|
requestAnimationFrame(() => {
|
||||||
this.store.setForCurrentAutosuggest({ autosuggestSelecting: true })
|
const id = `the-autosuggest-container-${realm}`
|
||||||
this.fire('autosuggestItemSelected')
|
this._element = document.getElementById(id)
|
||||||
selectAutosuggestItem(item)
|
if (!this._element) {
|
||||||
|
this._element = document.createElement('div')
|
||||||
|
this._element.id = id
|
||||||
|
const parent = realm === 'dialog' ? document.querySelector('.modal-dialog-contents') : document.body
|
||||||
|
parent.appendChild(this._element) // write
|
||||||
|
}
|
||||||
|
requestPostAnimationFrame(() => {
|
||||||
|
const { left, top } = this.calculateLeftAndTop()
|
||||||
|
this._autosuggest = new ComposeAutosuggestContainer({
|
||||||
|
target: this._element,
|
||||||
|
data: {
|
||||||
|
realm,
|
||||||
|
text,
|
||||||
|
dialogId,
|
||||||
|
left,
|
||||||
|
top
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.observe('text', text => {
|
||||||
|
this._autosuggest.set({ text })
|
||||||
|
}, { init: false })
|
||||||
|
this.observe('shouldBeShown', shouldBeShown => {
|
||||||
|
if (shouldBeShown) {
|
||||||
|
this.onResize() // just in case the window size changed while we weren't focused
|
||||||
|
}
|
||||||
|
})
|
||||||
|
registerResizeListener(this.onResize)
|
||||||
|
document.addEventListener('scroll', this.onResize)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
doResize () {
|
||||||
|
const { shouldBeShown } = this.get()
|
||||||
|
if (this._autosuggest && shouldBeShown) {
|
||||||
|
const { left, top } = this.calculateLeftAndTop()
|
||||||
|
console.log('updating autosuggest position', { left, top })
|
||||||
|
this._autosuggest.set({ left, top })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
calculateLeftAndTop () {
|
||||||
|
const { anchor } = this.refs
|
||||||
|
const { realm } = this.get()
|
||||||
|
const { left, bottom } = anchor.getBoundingClientRect()
|
||||||
|
const yOffset = realm === 'dialog' ? 0 : getScrollContainer().scrollTop
|
||||||
|
return {
|
||||||
|
left,
|
||||||
|
top: bottom + yOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
store: () => store,
|
||||||
computed: {
|
computed: {
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
composeSelectionStart: ({ $autosuggestData_composeSelectionStart, $currentInstance, realm }) => (
|
|
||||||
get($autosuggestData_composeSelectionStart, [$currentInstance, realm], 0)
|
|
||||||
),
|
|
||||||
composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => (
|
composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => (
|
||||||
get($autosuggestData_composeFocused, [$currentInstance, realm], false)
|
get($autosuggestData_composeFocused, [$currentInstance, realm], false)
|
||||||
),
|
),
|
||||||
autosuggestSearchResults: ({ $autosuggestData_autosuggestSearchResults, $currentInstance, realm }) => (
|
|
||||||
get($autosuggestData_autosuggestSearchResults, [$currentInstance, realm], [])
|
|
||||||
),
|
|
||||||
autosuggestType: ({ $autosuggestData_autosuggestType, $currentInstance, realm }) => (
|
|
||||||
get($autosuggestData_autosuggestType, [$currentInstance, realm])
|
|
||||||
),
|
|
||||||
autosuggestSelected: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
|
|
||||||
get($autosuggestData_autosuggestSelected, [$currentInstance, realm], 0)
|
|
||||||
),
|
|
||||||
autosuggestSearchText: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
|
|
||||||
get($autosuggestData_autosuggestSelected, [$currentInstance, realm])
|
|
||||||
),
|
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
shouldBeShown: ({ realm, $autosuggestShown, composeFocused }) => (
|
shouldBeShown: ({ $autosuggestShown, composeFocused }) => (
|
||||||
!!($autosuggestShown && composeFocused)
|
!!($autosuggestShown && composeFocused)
|
||||||
)
|
)
|
||||||
},
|
|
||||||
data: () => ({
|
|
||||||
shown: false
|
|
||||||
}),
|
|
||||||
store: () => store,
|
|
||||||
components: {
|
|
||||||
ComposeAutosuggestionList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
125
src/routes/_components/compose/ComposeAutosuggestContainer.html
Normal file
125
src/routes/_components/compose/ComposeAutosuggestContainer.html
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<div class="compose-autosuggest {shown ? '' : 'not-shown'} {realm === 'dialog' ? 'is-dialog' : ''}"
|
||||||
|
style="top: {top}px; --autosuggest-input-left: {left}px;"
|
||||||
|
>
|
||||||
|
<ComposeAutosuggestionList
|
||||||
|
items={autosuggestSearchResults}
|
||||||
|
on:click="onClick(event)"
|
||||||
|
type={autosuggestType}
|
||||||
|
selected={autosuggestSelected}
|
||||||
|
{realm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.compose-autosuggest {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 90;
|
||||||
|
--autosuggest-input-left: 0; /* overridden by "left" prop passed in */
|
||||||
|
--autosuggest-left-offset: 5px;
|
||||||
|
/* In desktop mode, the autosuggest tracks the position of the input (the "left" prop passed in). */
|
||||||
|
left: calc(var(--autosuggest-input-left) + var(--autosuggest-left-offset));
|
||||||
|
}
|
||||||
|
.compose-autosuggest.not-shown {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* desktop styles */
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
.compose-autosuggest {
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mobile size */
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.compose-autosuggest {
|
||||||
|
/* on mobile, make it fill the viewport width */
|
||||||
|
--autosuggest-left-offset: 10px;
|
||||||
|
left: var(--autosuggest-left-offset);
|
||||||
|
width: calc(100vw - (2 * var(--autosuggest-left-offset)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tiny mobile size */
|
||||||
|
@media (max-width: 240px) {
|
||||||
|
.compose-autosuggest {
|
||||||
|
--autosuggest-left-offset: 5px; /* make it bigger on tiny devices */
|
||||||
|
}
|
||||||
|
.compose-autosuggest.is-dialog {
|
||||||
|
--autosuggest-left-offset: 10px; /* more padding in dialogs */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import { store } from '../../_store/store'
|
||||||
|
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
|
||||||
|
import { get } from '../../_utils/lodash-lite'
|
||||||
|
import { selectAutosuggestItem } from '../../_actions/autosuggest'
|
||||||
|
import { observe } from 'svelte-extras'
|
||||||
|
import { once } from '../../_utils/once'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
oncreate () {
|
||||||
|
this._promiseChain = Promise.resolve()
|
||||||
|
this.observe('shouldBeShown', (shouldBeShown) => {
|
||||||
|
// TODO: hack so that when the user clicks the button, and the textarea blurs,
|
||||||
|
// we don't immediately hide the dropdown which would cause the click to get lost
|
||||||
|
this._promiseChain = this._promiseChain.then(() => {
|
||||||
|
if (!shouldBeShown) {
|
||||||
|
return Promise.race([
|
||||||
|
new Promise(resolve => setTimeout(resolve, 200)),
|
||||||
|
new Promise(resolve => this.once('autosuggestItemSelected', resolve))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
this.set({ shown: shouldBeShown })
|
||||||
|
this.store.setForCurrentAutosuggest({ autosuggestSelecting: false })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
observe,
|
||||||
|
once,
|
||||||
|
onClick (item) {
|
||||||
|
/* autosuggestSelecting prevents a flash of searched content */
|
||||||
|
this.store.setForCurrentAutosuggest({ autosuggestSelecting: true })
|
||||||
|
this.fire('autosuggestItemSelected')
|
||||||
|
selectAutosuggestItem(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
composeSelectionStart: ({ $autosuggestData_composeSelectionStart, $currentInstance, realm }) => (
|
||||||
|
get($autosuggestData_composeSelectionStart, [$currentInstance, realm], 0)
|
||||||
|
),
|
||||||
|
composeFocused: ({ $autosuggestData_composeFocused, $currentInstance, realm }) => (
|
||||||
|
get($autosuggestData_composeFocused, [$currentInstance, realm], false)
|
||||||
|
),
|
||||||
|
autosuggestSearchResults: ({ $autosuggestData_autosuggestSearchResults, $currentInstance, realm }) => (
|
||||||
|
get($autosuggestData_autosuggestSearchResults, [$currentInstance, realm], [])
|
||||||
|
),
|
||||||
|
autosuggestType: ({ $autosuggestData_autosuggestType, $currentInstance, realm }) => (
|
||||||
|
get($autosuggestData_autosuggestType, [$currentInstance, realm])
|
||||||
|
),
|
||||||
|
autosuggestSelected: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
|
||||||
|
get($autosuggestData_autosuggestSelected, [$currentInstance, realm], 0)
|
||||||
|
),
|
||||||
|
autosuggestSearchText: ({ $autosuggestData_autosuggestSelected, $currentInstance, realm }) => (
|
||||||
|
get($autosuggestData_autosuggestSelected, [$currentInstance, realm])
|
||||||
|
),
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
shouldBeShown: ({ $autosuggestShown, composeFocused }) => (
|
||||||
|
!!($autosuggestShown && composeFocused)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
shown: false,
|
||||||
|
top: 0
|
||||||
|
}),
|
||||||
|
store: () => store,
|
||||||
|
components: {
|
||||||
|
ComposeAutosuggestionList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -12,7 +12,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
<ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
|
<ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
|
||||||
<ComposeLengthGauge {length} {overLimit} />
|
<ComposeLengthGauge {length} {overLimit} />
|
||||||
<ComposeAutosuggest {realm} {text} />
|
<ComposeAutosuggest {realm} {text} {dialogId} />
|
||||||
{#if poll && poll.options && poll.options.length}
|
{#if poll && poll.options && poll.options.length}
|
||||||
<div class="compose-poll-wrapper"
|
<div class="compose-poll-wrapper"
|
||||||
transition:slide="{duration: 333}">
|
transition:slide="{duration: 333}">
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
aria-activedescendant={activeDescendant}
|
aria-activedescendant={activeDescendant}
|
||||||
ref:textarea
|
ref:textarea
|
||||||
bind:value=rawText
|
bind:value=rawText
|
||||||
on:blur="onBlur()"
|
|
||||||
on:focus="onFocus()"
|
on:focus="onFocus()"
|
||||||
|
on:blur="onBlur()"
|
||||||
on:selectionChange="onSelectionChange(event)"
|
on:selectionChange="onSelectionChange(event)"
|
||||||
on:keydown="onKeydown(event)"
|
on:keydown="onKeydown(event)"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
|
@ -235,6 +235,7 @@
|
||||||
this._a11yDialog.show()
|
this._a11yDialog.show()
|
||||||
this.set({ fadedIn: true })
|
this.set({ fadedIn: true })
|
||||||
this.fire('show')
|
this.fire('show')
|
||||||
|
emit('dialogDidRender', id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue