add username autocomplete feature
This commit is contained in:
parent
5430fdd189
commit
6fc21e40bf
|
@ -47,3 +47,21 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
store.set({postingStatus: false})
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertUsername (realm, username, startIndex, endIndex) {
|
||||
let oldText = store.getComposeData(realm, 'text')
|
||||
let pre = oldText.substring(0, startIndex)
|
||||
let post = oldText.substring(endIndex)
|
||||
let newText = `${pre}@${username} ${post}`
|
||||
store.setComposeData(realm, {text: newText})
|
||||
}
|
||||
|
||||
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||
let selectionStart = store.get('composeSelectionStart')
|
||||
let searchText = store.get('composeAutosuggestionSearchText')
|
||||
let selection = store.get('composeAutosuggestionSelected') || 0
|
||||
let account = store.get('composeAutosuggestionSearchResults')[selection]
|
||||
let startIndex = selectionStart - searchText.length
|
||||
let endIndex = selectionStart
|
||||
await insertUsername(realm, account.acct, startIndex, endIndex)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
<div class="compose-autosuggest {{shown ? 'shown' : ''}}"
|
||||
aria-hidden="true" >
|
||||
<ComposeAutosuggestionList
|
||||
items="{{searchResults}}"
|
||||
on:click="onUserSelected(event)"
|
||||
:selected
|
||||
/>
|
||||
</div>
|
||||
<style>
|
||||
.compose-autosuggest {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.1s linear;
|
||||
min-width: 400px;
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
.compose-autosuggest.shown {
|
||||
pointer-events: auto;
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.compose-autosuggest {
|
||||
/* hack: move this over to the left on mobile so it's easier to see */
|
||||
transform: translateX(-58px); /* avatar size 48px + 10px padding */
|
||||
width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import { store } from '../../_store/store'
|
||||
import { database } from '../../_database/database'
|
||||
import { insertUsername } from '../../_actions/compose'
|
||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||
import { once } from '../../_utils/once'
|
||||
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
|
||||
|
||||
const SEARCH_RESULTS_LIMIT = 4
|
||||
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
||||
const MIN_PREFIX_LENGTH = 1
|
||||
const SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`)
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
// perf improves for input responsiveness
|
||||
this.observe('composeSelectionStart', () => {
|
||||
scheduleIdleTask(() => {
|
||||
this.set({composeSelectionStartDeferred: this.get('composeSelectionStart')})
|
||||
})
|
||||
})
|
||||
this.observe('composeFocused', (composeFocused) => {
|
||||
let updateFocusedState = () => {
|
||||
scheduleIdleTask(() => {
|
||||
this.set({composeFocusedDeferred: this.get('composeFocused')})
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
if (composeFocused) {
|
||||
updateFocusedState()
|
||||
} else {
|
||||
Promise.race([
|
||||
new Promise(resolve => setTimeout(resolve, 200)),
|
||||
new Promise(resolve => this.once('userSelected', resolve))
|
||||
]).then(updateFocusedState)
|
||||
}
|
||||
})
|
||||
this.observe('searchText', async searchText => {
|
||||
if (!searchText) {
|
||||
return
|
||||
}
|
||||
let results = await this.search(searchText)
|
||||
this.store.set({
|
||||
composeAutosuggestionSelected: 0,
|
||||
composeAutosuggestionSearchText: searchText,
|
||||
composeAutosuggestionSearchResults: results
|
||||
})
|
||||
})
|
||||
this.observe('shown', shown => {
|
||||
this.store.set({composeAutosuggestionShown: shown})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
once: once,
|
||||
onUserSelected(account) {
|
||||
this.fire('userSelected')
|
||||
let realm = this.get('realm')
|
||||
let selectionStart = this.store.get('composeSelectionStart')
|
||||
let searchText = this.store.get('composeAutosuggestionSearchText')
|
||||
let startIndex = selectionStart - searchText.length
|
||||
let endIndex = selectionStart
|
||||
/* no await */ insertUsername(realm, account.acct, startIndex, endIndex)
|
||||
},
|
||||
async search(searchText) {
|
||||
let currentInstance = this.store.get('currentInstance')
|
||||
let results = await database.searchAccountsByUsername(
|
||||
currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT)
|
||||
return results.slice(0, SEARCH_RESULTS_LIMIT)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
composeSelectionStart: ($composeSelectionStart) => $composeSelectionStart,
|
||||
composeFocused: ($composeFocused) => $composeFocused,
|
||||
searchResults: ($composeAutosuggestionSearchResults) => $composeAutosuggestionSearchResults || [],
|
||||
selected: ($composeAutosuggestionSelected) => $composeAutosuggestionSelected || 0,
|
||||
searchText: (text, composeSelectionStartDeferred) => {
|
||||
let selectionStart = composeSelectionStartDeferred || 0
|
||||
if (!text || selectionStart < MIN_PREFIX_LENGTH) {
|
||||
return
|
||||
}
|
||||
|
||||
let match = text.substring(0, selectionStart).match(SEARCH_REGEX)
|
||||
return match && match[1]
|
||||
},
|
||||
shown: (composeFocusedDeferred, searchText, searchResults) => {
|
||||
return !!(composeFocusedDeferred &&
|
||||
searchText &&
|
||||
searchResults.length)
|
||||
}
|
||||
},
|
||||
store: () => store,
|
||||
components: {
|
||||
ComposeAutosuggestionList
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,90 @@
|
|||
<ul class="generic-user-list">
|
||||
{{#each items as account, i @id}}
|
||||
<li class="generic-user-list-item">
|
||||
<button class="generic-user-list-button {{i === selected ? 'selected' : ''}}"
|
||||
tabindex="0"
|
||||
on:click="fire('click', account)">
|
||||
<div class="generic-user-list-grid">
|
||||
<Avatar
|
||||
className="generic-user-list-item-avatar"
|
||||
size="small"
|
||||
:account
|
||||
/>
|
||||
<span class="generic-user-list-display-name">
|
||||
{{account.display_name || account.acct}}
|
||||
</span>
|
||||
<span class="generic-user-list-username">
|
||||
{{'@' + account.acct}}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<style>
|
||||
.generic-user-list {
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--compose-autosuggest-outline);
|
||||
}
|
||||
.generic-user-list-item {
|
||||
border-bottom: 1px solid var(--compose-autosuggest-outline);
|
||||
display: flex;
|
||||
}
|
||||
.generic-user-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.generic-user-list-button {
|
||||
padding: 10px;
|
||||
background: var(--settings-list-item-bg);
|
||||
border: none;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.generic-user-list-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-areas: "avatar display-name"
|
||||
"avatar username";
|
||||
grid-template-columns: min-content 1fr;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 5px;
|
||||
}
|
||||
:global(.generic-user-list-item-avatar) {
|
||||
grid-area: avatar;
|
||||
}
|
||||
.generic-user-list-display-name {
|
||||
grid-area: display-name;
|
||||
font-size: 1.1em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.generic-user-list-username {
|
||||
grid-area: username;
|
||||
font-size: 1em;
|
||||
color: var(--deemphasized-text-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
.generic-user-list-button:hover, .generic-user-list-button.selected {
|
||||
background: var(--compose-autosuggest-item-hover);
|
||||
}
|
||||
.generic-user-list-button:active {
|
||||
background: var(--compose-autosuggest-item-active);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import Avatar from '../Avatar.html'
|
||||
export default {
|
||||
components: {
|
||||
Avatar
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -8,7 +8,7 @@
|
|||
{{/if}}
|
||||
<ComposeInput :realm :text :autoFocus />
|
||||
<ComposeLengthGauge :length :overLimit />
|
||||
<ComposeToolbar :realm :postPrivacy :media :contentWarningShown />
|
||||
<ComposeToolbar :realm :postPrivacy :media :contentWarningShown :text />
|
||||
<ComposeLengthIndicator :length :overLimit />
|
||||
<ComposeMedia :realm :media />
|
||||
<ComposeButton :length :overLimit on:click="onClickPostButton()" />
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
ref:textarea
|
||||
bind:value=rawText
|
||||
on:blur="onBlur()"
|
||||
on:focus="onFocus()"
|
||||
on:selectionChange="onSelectionChange(event)"
|
||||
on:keydown="onKeydown(event)"
|
||||
></textarea>
|
||||
<style>
|
||||
.compose-box-input {
|
||||
|
@ -29,6 +32,8 @@
|
|||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { mark, stop } from '../../_utils/marks'
|
||||
import { selectionChange } from '../../_utils/events'
|
||||
import { clickSelectedAutosuggestionUsername } from '../../_actions/compose'
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
|
@ -82,7 +87,53 @@
|
|||
stop('autosize.destroy()')
|
||||
},
|
||||
onBlur() {
|
||||
this.store.set({composeSelectionStart: this.refs.textarea.selectionStart})
|
||||
this.store.set({composeFocused: false})
|
||||
},
|
||||
onFocus() {
|
||||
this.store.set({composeFocused: true})
|
||||
},
|
||||
onSelectionChange(selectionStart) {
|
||||
this.store.set({composeSelectionStart: selectionStart})
|
||||
},
|
||||
onKeydown(e) {
|
||||
let { keyCode } = e
|
||||
switch (keyCode) {
|
||||
case 9: // tab
|
||||
case 13: //enter
|
||||
this.clickSelectedAutosuggestion(e)
|
||||
break
|
||||
case 38: // up
|
||||
this.incrementAutosuggestSelected(-1, e)
|
||||
break
|
||||
case 40: // down
|
||||
this.incrementAutosuggestSelected(1, e)
|
||||
break
|
||||
default:
|
||||
}
|
||||
},
|
||||
clickSelectedAutosuggestion(event) {
|
||||
let autosuggestionShown = this.store.get('composeAutosuggestionShown')
|
||||
if (!autosuggestionShown) {
|
||||
return
|
||||
}
|
||||
clickSelectedAutosuggestionUsername(this.get('realm'))
|
||||
event.preventDefault()
|
||||
},
|
||||
incrementAutosuggestSelected(increment, event) {
|
||||
let autosuggestionShown = this.store.get('composeAutosuggestionShown')
|
||||
if (!autosuggestionShown) {
|
||||
return
|
||||
}
|
||||
let selected = this.store.get('composeAutosuggestionSelected') || 0
|
||||
let searchResults = this.store.get('composeAutosuggestionSearchResults') || []
|
||||
selected += increment
|
||||
if (selected >= 0) {
|
||||
selected = selected % searchResults.length
|
||||
} else {
|
||||
selected = searchResults.length + selected
|
||||
}
|
||||
this.store.set({composeAutosuggestionSelected: selected})
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
store: () => store,
|
||||
|
@ -91,6 +142,9 @@
|
|||
}),
|
||||
computed: {
|
||||
postedStatusForRealm: ($postedStatusForRealm) => $postedStatusForRealm
|
||||
},
|
||||
events: {
|
||||
selectionChange
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,41 +1,55 @@
|
|||
<div class="compose-box-toolbar">
|
||||
<IconButton
|
||||
label="Insert emoji"
|
||||
href="#fa-smile"
|
||||
on:click="onEmojiClick()"
|
||||
/>
|
||||
<IconButton
|
||||
className="{{$uploadingMedia ? 'spin' : ''}}"
|
||||
label="Add media"
|
||||
href="{{$uploadingMedia ? '#fa-spinner' : '#fa-camera'}}"
|
||||
on:click="onMediaClick()"
|
||||
disabled="{{$uploadingMedia || (media.length === 4)}}"
|
||||
/>
|
||||
<IconButton
|
||||
label="Adjust privacy (currently {{postPrivacy.label}})"
|
||||
href="{{postPrivacy.icon}}"
|
||||
on:click="onPostPrivacyClick()"
|
||||
/>
|
||||
<IconButton
|
||||
label="{{contentWarningShown ? 'Remove content warning' : 'Add content warning'}}"
|
||||
href="#fa-exclamation-triangle"
|
||||
on:click="onContentWarningClick()"
|
||||
pressable="true"
|
||||
pressed="{{contentWarningShown}}"
|
||||
/>
|
||||
<div class="compose-box-toolbar-items">
|
||||
<IconButton
|
||||
label="Insert emoji"
|
||||
href="#fa-smile"
|
||||
on:click="onEmojiClick()"
|
||||
/>
|
||||
<IconButton
|
||||
className="{{$uploadingMedia ? 'spin' : ''}}"
|
||||
label="Add media"
|
||||
href="{{$uploadingMedia ? '#fa-spinner' : '#fa-camera'}}"
|
||||
on:click="onMediaClick()"
|
||||
disabled="{{$uploadingMedia || (media.length === 4)}}"
|
||||
/>
|
||||
<IconButton
|
||||
label="Adjust privacy (currently {{postPrivacy.label}})"
|
||||
href="{{postPrivacy.icon}}"
|
||||
on:click="onPostPrivacyClick()"
|
||||
/>
|
||||
<IconButton
|
||||
label="{{contentWarningShown ? 'Remove content warning' : 'Add content warning'}}"
|
||||
href="#fa-exclamation-triangle"
|
||||
on:click="onContentWarningClick()"
|
||||
pressable="true"
|
||||
pressed="{{contentWarningShown}}"
|
||||
/>
|
||||
</div>
|
||||
<input ref:input
|
||||
on:change="onFileChange(event)"
|
||||
style="display: none;"
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,image/jpeg,image/png,image/gif,video/webm,video/mp4">
|
||||
<div class="compose-autosuggest-wrapper">
|
||||
<ComposeAutosuggest :realm :text />
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.compose-box-toolbar {
|
||||
grid-area: toolbar;
|
||||
position: relative;
|
||||
align-self: center;
|
||||
}
|
||||
.compose-box-toolbar-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.compose-autosuggest-wrapper {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 0;
|
||||
z-index: 90;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import IconButton from '../IconButton.html'
|
||||
|
@ -44,6 +58,7 @@
|
|||
import { importDialogs } from '../../_utils/asyncModules'
|
||||
import { doMediaUpload } from '../../_actions/media'
|
||||
import { toggleContentWarningShown } from '../../_actions/contentWarnings'
|
||||
import ComposeAutosuggest from './ComposeAutosuggest.html'
|
||||
|
||||
export default {
|
||||
oncreate() {
|
||||
|
@ -58,7 +73,8 @@
|
|||
}
|
||||
},
|
||||
components: {
|
||||
IconButton
|
||||
IconButton,
|
||||
ComposeAutosuggest
|
||||
},
|
||||
store: () => store,
|
||||
methods: {
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { ACCOUNTS_STORE, RELATIONSHIPS_STORE } from './constants'
|
||||
import {
|
||||
ACCOUNTS_STORE, RELATIONSHIPS_STORE, USERNAME_LOWERCASE
|
||||
} from './constants'
|
||||
import { accountsCache, relationshipsCache } from './cache'
|
||||
import { cloneForStorage, getGenericEntityWithId, setGenericEntityWithId } from './helpers'
|
||||
import { dbPromise, getDatabase } from './databaseLifecycle'
|
||||
import { createAccountUsernamePrefixKeyRange } from './keys'
|
||||
|
||||
export async function getAccount (instanceName, accountId) {
|
||||
return getGenericEntityWithId(ACCOUNTS_STORE, accountsCache, instanceName, accountId)
|
||||
|
@ -17,3 +21,25 @@ export async function getRelationship (instanceName, accountId) {
|
|||
export async function setRelationship (instanceName, relationship) {
|
||||
return setGenericEntityWithId(RELATIONSHIPS_STORE, relationshipsCache, instanceName, cloneForStorage(relationship))
|
||||
}
|
||||
|
||||
export async function searchAccountsByUsername (instanceName, usernamePrefix, limit = 20) {
|
||||
const db = await getDatabase(instanceName)
|
||||
return dbPromise(db, ACCOUNTS_STORE, 'readonly', (accountsStore, callback) => {
|
||||
let keyRange = createAccountUsernamePrefixKeyRange(usernamePrefix.toLowerCase())
|
||||
accountsStore.index(USERNAME_LOWERCASE).getAll(keyRange, limit).onsuccess = e => {
|
||||
let results = e.target.result
|
||||
results = results.sort((a, b) => {
|
||||
// accounts you're following go first
|
||||
if (a.following !== b.following) {
|
||||
return a.following ? -1 : 1
|
||||
}
|
||||
// after that, just sort by username
|
||||
if (a[USERNAME_LOWERCASE] !== b[USERNAME_LOWERCASE]) {
|
||||
return a[USERNAME_LOWERCASE] < b[USERNAME_LOWERCASE] ? -1 : 1
|
||||
}
|
||||
return 0 // eslint-disable-line
|
||||
})
|
||||
callback(results)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -12,3 +12,4 @@ export const TIMESTAMP = '__pinafore_ts'
|
|||
export const ACCOUNT_ID = '__pinafore_acct_id'
|
||||
export const STATUS_ID = '__pinafore_status_id'
|
||||
export const REBLOG_ID = '__pinafore_reblog_id'
|
||||
export const USERNAME_LOWERCASE = '__pinafore_acct_lc'
|
||||
|
|
|
@ -10,7 +10,8 @@ import {
|
|||
TIMESTAMP,
|
||||
REBLOG_ID,
|
||||
THREADS_STORE,
|
||||
STATUS_ID
|
||||
STATUS_ID,
|
||||
USERNAME_LOWERCASE
|
||||
} from './constants'
|
||||
|
||||
import forEach from 'lodash/forEach'
|
||||
|
@ -18,7 +19,9 @@ import forEach from 'lodash/forEach'
|
|||
const openReqs = {}
|
||||
const databaseCache = {}
|
||||
|
||||
const DB_VERSION = 9
|
||||
const DB_VERSION_INITIAL = 9
|
||||
const DB_VERSION_SEARCH_ACCOUNTS = 10
|
||||
const DB_VERSION_CURRENT = 10
|
||||
|
||||
export function getDatabase (instanceName) {
|
||||
if (!instanceName) {
|
||||
|
@ -29,7 +32,7 @@ export function getDatabase (instanceName) {
|
|||
}
|
||||
|
||||
databaseCache[instanceName] = new Promise((resolve, reject) => {
|
||||
let req = indexedDB.open(instanceName, DB_VERSION)
|
||||
let req = indexedDB.open(instanceName, DB_VERSION_CURRENT)
|
||||
openReqs[instanceName] = req
|
||||
req.onerror = reject
|
||||
req.onblocked = () => {
|
||||
|
@ -37,6 +40,7 @@ export function getDatabase (instanceName) {
|
|||
}
|
||||
req.onupgradeneeded = (e) => {
|
||||
let db = req.result
|
||||
let tx = e.currentTarget.transaction
|
||||
|
||||
function createObjectStore (name, init, indexes) {
|
||||
let store = init
|
||||
|
@ -49,7 +53,7 @@ export function getDatabase (instanceName) {
|
|||
}
|
||||
}
|
||||
|
||||
if (e.oldVersion < DB_VERSION) {
|
||||
if (e.oldVersion < DB_VERSION_INITIAL) {
|
||||
createObjectStore(STATUSES_STORE, {keyPath: 'id'}, {
|
||||
[TIMESTAMP]: TIMESTAMP,
|
||||
[REBLOG_ID]: REBLOG_ID
|
||||
|
@ -78,6 +82,10 @@ export function getDatabase (instanceName) {
|
|||
})
|
||||
createObjectStore(META_STORE)
|
||||
}
|
||||
if (e.oldVersion < DB_VERSION_SEARCH_ACCOUNTS) {
|
||||
tx.objectStore(ACCOUNTS_STORE)
|
||||
.createIndex(USERNAME_LOWERCASE, USERNAME_LOWERCASE)
|
||||
}
|
||||
}
|
||||
req.onsuccess = () => resolve(req.result)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { dbPromise, getDatabase } from './databaseLifecycle'
|
||||
import { getInCache, hasInCache, setInCache } from './cache'
|
||||
import { ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP } from './constants'
|
||||
import { ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP, USERNAME_LOWERCASE } from './constants'
|
||||
|
||||
export async function getGenericEntityWithId (store, cache, instanceName, id) {
|
||||
if (hasInCache(cache, instanceName, id)) {
|
||||
|
@ -41,6 +41,10 @@ export function cloneForStorage (obj) {
|
|||
case 'reblog':
|
||||
res[REBLOG_ID] = value.id
|
||||
break
|
||||
case 'acct':
|
||||
res[key] = value
|
||||
res[USERNAME_LOWERCASE] = value.toLowerCase()
|
||||
break
|
||||
default:
|
||||
res[key] = value
|
||||
break
|
||||
|
|
|
@ -45,3 +45,14 @@ export function createPinnedStatusKeyRange (accountId) {
|
|||
accountId + '\u0000\uffff'
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
// accounts
|
||||
//
|
||||
|
||||
export function createAccountUsernamePrefixKeyRange (accountUsernamePrefix) {
|
||||
return IDBKeyRange.bound(
|
||||
accountUsernamePrefix,
|
||||
accountUsernamePrefix + '\uffff'
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,3 +52,20 @@ export function blurWithCapture (node, callback) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function selectionChange (node, callback) {
|
||||
let events = ['keyup', 'click', 'focus', 'blur']
|
||||
let listener = () => {
|
||||
callback(node.selectionStart)
|
||||
}
|
||||
for (let event of events) {
|
||||
node.addEventListener(event, listener)
|
||||
}
|
||||
return {
|
||||
teardown () {
|
||||
for (let event of events) {
|
||||
node.removeEventListener(event, listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// svelte helper to add a .once() method similar to .on, but only fires once
|
||||
|
||||
export function once (eventName, callback) {
|
||||
let listener = this.on(eventName, eventValue => {
|
||||
listener.cancel()
|
||||
callback(eventValue)
|
||||
})
|
||||
}
|
|
@ -74,4 +74,8 @@
|
|||
--muted-modal-bg: transparent;
|
||||
--muted-modal-focus: #999;
|
||||
--muted-modal-hover: rgba(255, 255, 255, 0.2);
|
||||
|
||||
--compose-autosuggest-item-hover: $compose-background;
|
||||
--compose-autosuggest-item-active: darken($compose-background, 5%);
|
||||
--compose-autosuggest-outline: lighten($focus-outline, 5%);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 30%);
|
||||
$compose-background: lighten($main-theme-color, 32%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 15%);
|
||||
$compose-background: lighten($main-theme-color, 17%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 30%);
|
||||
$compose-background: lighten($main-theme-color, 32%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 15%);
|
||||
$compose-background: lighten($main-theme-color, 17%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 30%);
|
||||
$compose-background: lighten($main-theme-color, 32%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 30%);
|
||||
$compose-background: lighten($main-theme-color, 32%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 30%);
|
||||
$compose-background: lighten($main-theme-color, 32%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
|||
$toast-border: #fafafa;
|
||||
$toast-bg: #333;
|
||||
$focus-outline: lighten($main-theme-color, 50%);
|
||||
$compose-background: lighten($main-theme-color, 52%);
|
||||
|
||||
@import "_base.scss";
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
<style>
|
||||
/* auto-generated w/ build-sass.js */
|
||||
body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#90a8ee;--action-button-fill-color-hover:#a2b6f0;--action-button-fill-color-active:#577ae4;--action-button-fill-color-pressed:#2351dc;--action-button-fill-color-pressed-hover:#3862e0;--action-button-fill-color-pressed-active:#1d44b8;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#c5d1f6;--very-deemphasized-link-color:rgba(65,105,225,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#d2dcf8;--main-theme-color:#4169e1;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2)}
|
||||
body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#90a8ee;--action-button-fill-color-hover:#a2b6f0;--action-button-fill-color-active:#577ae4;--action-button-fill-color-pressed:#2351dc;--action-button-fill-color-pressed-hover:#3862e0;--action-button-fill-color-pressed-active:#1d44b8;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#c5d1f6;--very-deemphasized-link-color:rgba(65,105,225,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#d2dcf8;--main-theme-color:#4169e1;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2);--compose-autosuggest-item-hover:#ced8f7;--compose-autosuggest-item-active:#b8c7f4;--compose-autosuggest-outline:#dbe3f9}
|
||||
body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);position:fixed;left:0;right:0;bottom:0;top:0}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;will-change:transform;position:absolute;top:72px;left:0;right:0;bottom:0}@media (max-width: 767px){.container{top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:20px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}50%{transform:rotate(180deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 2s infinite linear}.ellipsis::after{content:"\2026"}
|
||||
body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline,body.theme-gecko.offline{--button-primary-bg:#ababab;--button-primary-text:#fff;--button-primary-border:#4d4d4d;--button-primary-bg-active:#9c9c9c;--button-primary-bg-hover:#b0b0b0;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#999;--main-bg:#fff;--body-bg:#fafafa;--body-text-color:#333;--main-border:#dadada;--svg-fill:#999;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#999;--nav-border:gray;--nav-a-border:#999;--nav-a-selected-border:#fff;--nav-a-selected-bg:#b3b3b3;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#bfbfbf;--nav-a-bg-hover:#a6a6a6;--nav-a-border-hover:#999;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#c7c7c7;--action-button-fill-color-hover:#d1d1d1;--action-button-fill-color-active:#a6a6a6;--action-button-fill-color-pressed:#878787;--action-button-fill-color-pressed-hover:#949494;--action-button-fill-color-pressed-active:#737373;--settings-list-item-bg:#fff;--settings-list-item-text:#999;--settings-list-item-text-hover:#999;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#bfbfbf;--very-deemphasized-link-color:rgba(153,153,153,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#ededed;--main-theme-color:#999;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2)}
|
||||
body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline,body.theme-gecko.offline{--button-primary-bg:#ababab;--button-primary-text:#fff;--button-primary-border:#4d4d4d;--button-primary-bg-active:#9c9c9c;--button-primary-bg-hover:#b0b0b0;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#999;--main-bg:#fff;--body-bg:#fafafa;--body-text-color:#333;--main-border:#dadada;--svg-fill:#999;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#999;--nav-border:gray;--nav-a-border:#999;--nav-a-selected-border:#fff;--nav-a-selected-bg:#b3b3b3;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#bfbfbf;--nav-a-bg-hover:#a6a6a6;--nav-a-border-hover:#999;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#c7c7c7;--action-button-fill-color-hover:#d1d1d1;--action-button-fill-color-active:#a6a6a6;--action-button-fill-color-pressed:#878787;--action-button-fill-color-pressed-hover:#949494;--action-button-fill-color-pressed-active:#737373;--settings-list-item-bg:#fff;--settings-list-item-text:#999;--settings-list-item-text-hover:#999;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#bfbfbf;--very-deemphasized-link-color:rgba(153,153,153,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#ededed;--main-theme-color:#999;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2);--compose-autosuggest-item-hover:#c4c4c4;--compose-autosuggest-item-active:#b8b8b8;--compose-autosuggest-outline:#ccc}
|
||||
|
||||
</style>
|
||||
|
||||
|
|
Loading…
Reference in New Issue