feat: add search in environment selector and sidebar (#4872)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
1a2b9516c9
commit
e43d0f1b70
5 changed files with 288 additions and 71 deletions
|
|
@ -68,7 +68,6 @@
|
|||
"turn_on": "Turn on",
|
||||
"undo": "Undo",
|
||||
"yes": "Yes",
|
||||
"confirm": "Confirm",
|
||||
"verify": "Verify",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable"
|
||||
|
|
@ -367,6 +366,7 @@
|
|||
"protocols": "Protocols are empty",
|
||||
"request_variables": "This request does not have any request variables",
|
||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||
"search_environment": "No matching environment found for",
|
||||
"secret_environments": "Secrets are not synced to Hoppscotch",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
|
|
|
|||
|
|
@ -26,12 +26,20 @@
|
|||
<div
|
||||
ref="envSelectorActions"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
class="flex flex-col space-y-2 focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<SmartEnvInput
|
||||
v-model="filterText"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
:context-menu-enabled="false"
|
||||
class="border border-dividerDark focus:border-primaryDark rounded"
|
||||
:readonly="isFilterInputDisabled"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="!isScopeSelector"
|
||||
class="my-2"
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||
|
|
@ -65,7 +73,7 @@
|
|||
/>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedEnvTab"
|
||||
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary ${
|
||||
:styles="`sticky overflow-x-auto mb-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary ${
|
||||
!isTeamSelected || workspace.type === 'personal'
|
||||
? 'bg-primaryLight'
|
||||
: ''
|
||||
|
|
@ -77,10 +85,7 @@
|
|||
:label="`${t('environment.my_environments')}`"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="{
|
||||
env,
|
||||
index,
|
||||
} in alphabeticallySortedPersonalEnvironments"
|
||||
v-for="{ env, index } in filteredAndAlphabetizedPersonalEnvs"
|
||||
:key="`gen-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="env.name"
|
||||
|
|
@ -97,12 +102,30 @@
|
|||
"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="alphabeticallySortedPersonalEnvironments.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
/>
|
||||
v-if="filteredAndAlphabetizedPersonalEnvs.length === 0"
|
||||
class="break-words"
|
||||
:src="
|
||||
filterText
|
||||
? undefined
|
||||
: `/images/states/${colorMode.value}/blockchain.svg`
|
||||
"
|
||||
:alt="
|
||||
filterText
|
||||
? `${t('empty.search_environment')}`
|
||||
: t('empty.environments')
|
||||
"
|
||||
:text="
|
||||
filterText
|
||||
? `${t('empty.search_environment')} '${filterText}'`
|
||||
: t('empty.environments')
|
||||
"
|
||||
>
|
||||
<template v-if="filterText" #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab
|
||||
:id="'team-environments'"
|
||||
:label="`${t('environment.team_environments')}`"
|
||||
|
|
@ -119,7 +142,7 @@
|
|||
</div>
|
||||
<div v-if="isTeamSelected" class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
v-for="{ env, index } in alphabeticallySortedTeamEnvironments"
|
||||
v-for="{ env, index } in filteredAndAlphabetizedTeamEnvs"
|
||||
:key="`gen-team-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="env.environment.name"
|
||||
|
|
@ -136,11 +159,28 @@
|
|||
"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="alphabeticallySortedTeamEnvironments.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
/>
|
||||
v-if="filteredAndAlphabetizedTeamEnvs.length === 0"
|
||||
class="break-words"
|
||||
:src="
|
||||
filteredAndAlphabetizedTeamEnvs.length === 0 && !filterText
|
||||
? `/images/states/${colorMode.value}/blockchain.svg`
|
||||
: undefined
|
||||
"
|
||||
:alt="
|
||||
filterText
|
||||
? `${t('empty.search_environment')}`
|
||||
: t('empty.environments')
|
||||
"
|
||||
:text="
|
||||
filterText
|
||||
? `${t('empty.search_environment')} '${filterText}'`
|
||||
: t('empty.environments')
|
||||
"
|
||||
>
|
||||
<template v-if="filterText" #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
<div
|
||||
v-if="!teamListLoading && teamAdapterError"
|
||||
|
|
@ -356,6 +396,8 @@ const colorMode = useColorMode()
|
|||
|
||||
type EnvironmentType = "my-environments" | "team-environments"
|
||||
|
||||
const filterText = ref("")
|
||||
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
|
||||
const workspaceService = useService(WorkspaceService)
|
||||
|
|
@ -398,14 +440,44 @@ const teamEnvironmentList = useReadonlyStream(
|
|||
[]
|
||||
)
|
||||
|
||||
// Sort environments alphabetically by default
|
||||
const alphabeticallySortedPersonalEnvironments = computed(() =>
|
||||
sortPersonalEnvironmentsAlphabetically(myEnvironments.value, "asc")
|
||||
)
|
||||
// Sort environments alphabetically by default and filter based on search
|
||||
const filteredAndAlphabetizedPersonalEnvs = computed(() => {
|
||||
const envs = sortPersonalEnvironmentsAlphabetically(
|
||||
myEnvironments.value,
|
||||
"asc"
|
||||
)
|
||||
|
||||
const alphabeticallySortedTeamEnvironments = computed(() =>
|
||||
sortTeamEnvironmentsAlphabetically(teamEnvironmentList.value, "asc")
|
||||
)
|
||||
if (selectedEnvTab.value !== "my-environments" || !filterText.value)
|
||||
return envs
|
||||
|
||||
// Ensure specifying whitespace characters alone result in the empty state for no search results
|
||||
const trimmedFilterText = filterText.value.trim().toLowerCase()
|
||||
|
||||
return envs.filter(({ env }) =>
|
||||
trimmedFilterText
|
||||
? env.name.toLowerCase().includes(trimmedFilterText)
|
||||
: false
|
||||
)
|
||||
})
|
||||
|
||||
const filteredAndAlphabetizedTeamEnvs = computed(() => {
|
||||
const envs = sortTeamEnvironmentsAlphabetically(
|
||||
teamEnvironmentList.value,
|
||||
"asc"
|
||||
)
|
||||
|
||||
if (selectedEnvTab.value !== "team-environments" || !filterText.value)
|
||||
return envs
|
||||
|
||||
// Ensure specifying whitespace characters alone result in the empty state for no search results
|
||||
const trimmedFilterText = filterText.value.trim().toLowerCase()
|
||||
|
||||
return envs.filter(({ env }) =>
|
||||
trimmedFilterText
|
||||
? env.environment.name.toLowerCase().includes(trimmedFilterText)
|
||||
: false
|
||||
)
|
||||
})
|
||||
|
||||
const handleEnvironmentChange = (
|
||||
index: number,
|
||||
|
|
@ -603,6 +675,14 @@ const editGlobalEnv = () => {
|
|||
invokeAction("modals.global.environment.update", {})
|
||||
}
|
||||
|
||||
// Filter input disabled if no environments are available
|
||||
const isFilterInputDisabled = computed(() => {
|
||||
if (selectedEnvTab.value === "my-environments") {
|
||||
return myEnvironments.value.length === 0
|
||||
}
|
||||
return teamEnvironmentList.value.length === 0
|
||||
})
|
||||
|
||||
const editEnv = () => {
|
||||
if (selectedEnv.value.type === "MY_ENV" && selectedEnv.value.name) {
|
||||
invokeAction("modals.my.environment.edit", {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex w-full bg-transparent px-4 py-2 h-8 border-b border-dividerLight"
|
||||
:placeholder="t('action.search')"
|
||||
:disabled="!environments.length"
|
||||
/>
|
||||
<div
|
||||
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
|
||||
>
|
||||
|
|
@ -25,8 +33,9 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnvironmentsMyEnvironment
|
||||
v-for="{ env, index } in alphabeticallySortedPersonalEnvironments"
|
||||
v-for="{ env, index } in filteredAndAlphabetizedPersonalEnvs"
|
||||
:key="`environment-${index}`"
|
||||
:environment-index="index"
|
||||
:environment="env"
|
||||
|
|
@ -35,12 +44,28 @@
|
|||
@select-environment="selectEnvironment(index, env)"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!alphabeticallySortedPersonalEnvironments.length"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
v-if="filteredAndAlphabetizedPersonalEnvs.length === 0"
|
||||
:alt="
|
||||
filterText
|
||||
? `${t('empty.search_environment')}`
|
||||
: t('empty.environments')
|
||||
"
|
||||
:text="
|
||||
filterText
|
||||
? `${t('empty.search_environment')} '${filterText}'`
|
||||
: t('empty.environments')
|
||||
"
|
||||
:src="
|
||||
filterText
|
||||
? undefined
|
||||
: `/images/states/${colorMode.value}/blockchain.svg`
|
||||
"
|
||||
>
|
||||
<template #body>
|
||||
<template v-if="filterText" #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
</template>
|
||||
|
||||
<template v-else #body>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("environment.import_or_create") }}
|
||||
|
|
@ -106,10 +131,27 @@ const emit = defineEmits<{
|
|||
|
||||
const environments = useReadonlyStream(environments$, [])
|
||||
|
||||
// Sort environments alphabetically by default
|
||||
const alphabeticallySortedPersonalEnvironments = computed(() =>
|
||||
sortPersonalEnvironmentsAlphabetically(environments.value, "asc")
|
||||
)
|
||||
const filterText = ref("")
|
||||
|
||||
// Sort environments alphabetically by default and filter by search text
|
||||
const filteredAndAlphabetizedPersonalEnvs = computed(() => {
|
||||
const envs = sortPersonalEnvironmentsAlphabetically(environments.value, "asc")
|
||||
const rawFilter = filterText.value
|
||||
|
||||
// Ensure specifying whitespace characters alone result in the empty state for no search results
|
||||
const trimmedFilter = rawFilter.trim().toLowerCase()
|
||||
|
||||
// Whitespace-only input results in an empty state
|
||||
if (rawFilter && !trimmedFilter) return []
|
||||
|
||||
// No search text → Show all environments
|
||||
if (!trimmedFilter) return envs
|
||||
|
||||
// Filter environments based on search text
|
||||
return envs.filter(({ env }) =>
|
||||
env.name.toLowerCase().includes(trimmedFilter)
|
||||
)
|
||||
})
|
||||
|
||||
const showModalImportExport = ref(false)
|
||||
const showModalDetails = ref(false)
|
||||
|
|
@ -166,7 +208,7 @@ defineActionHandler(
|
|||
"modals.my.environment.edit",
|
||||
({ envName, variableName, isSecret }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const env = alphabeticallySortedPersonalEnvironments.value.find(
|
||||
const env = filteredAndAlphabetizedPersonalEnvs.value.find(
|
||||
({ env }) => env.name === envName
|
||||
)
|
||||
if (envName !== "Global" && env) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<input
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex w-full bg-transparent px-4 py-2 h-8 border-b border-dividerLight"
|
||||
:placeholder="t('action.search')"
|
||||
:disabled="loading || !teamEnvironments.length"
|
||||
/>
|
||||
<div
|
||||
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
|
||||
>
|
||||
|
|
@ -43,17 +51,40 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="adapterError" class="flex flex-col items-center py-4">
|
||||
<icon-lucide-help-circle class="svg-icons mb-4" />
|
||||
{{ t(getEnvActionErrorMessage(adapterError)) }}
|
||||
</div>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
!loading &&
|
||||
!alphabeticallySortedTeamEnvironments.length &&
|
||||
!adapterError
|
||||
v-else-if="filteredAndAlphabetizedTeamEnvs.length === 0"
|
||||
:alt="
|
||||
filterText
|
||||
? `${t('empty.search_environment')}`
|
||||
: t('empty.environments')
|
||||
"
|
||||
:text="
|
||||
filterText
|
||||
? `${t('empty.search_environment')} '${filterText}'`
|
||||
: t('empty.environments')
|
||||
"
|
||||
:src="
|
||||
filterText
|
||||
? undefined
|
||||
: `/images/states/${colorMode.value}/blockchain.svg`
|
||||
"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<template #body>
|
||||
<template v-if="filterText" #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
</template>
|
||||
|
||||
<template v-else #body>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-center text-secondaryLight">
|
||||
{{ t("environment.import_or_create") }}
|
||||
|
|
@ -81,10 +112,11 @@
|
|||
</div>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="!loading">
|
||||
|
||||
<div v-else>
|
||||
<EnvironmentsTeamsEnvironment
|
||||
v-for="{ env, index } in JSON.parse(
|
||||
JSON.stringify(alphabeticallySortedTeamEnvironments)
|
||||
JSON.stringify(filteredAndAlphabetizedTeamEnvs)
|
||||
)"
|
||||
:key="`environment-${index}`"
|
||||
:environment="env"
|
||||
|
|
@ -97,17 +129,7 @@
|
|||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<icon-lucide-help-circle class="svg-icons mb-4" />
|
||||
{{ t(getEnvActionErrorMessage(adapterError)) }}
|
||||
</div>
|
||||
|
||||
<EnvironmentsTeamsDetails
|
||||
:show="showModalDetails"
|
||||
:action="action"
|
||||
|
|
@ -120,9 +142,7 @@
|
|||
/>
|
||||
<EnvironmentsImportExport
|
||||
v-if="showModalImportExport"
|
||||
:team-environments="
|
||||
alphabeticallySortedTeamEnvironments.map(({ env }) => env)
|
||||
"
|
||||
:team-environments="filteredAndAlphabetizedTeamEnvs.map(({ env }) => env)"
|
||||
:team-id="team?.teamID"
|
||||
environment-type="TEAM_ENV"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
|
|
@ -168,11 +188,27 @@ const emit = defineEmits<{
|
|||
(e: "select-environment", data: HandleEnvChangeProp): void
|
||||
}>()
|
||||
|
||||
// Sort environments alphabetically by default
|
||||
const filterText = ref("")
|
||||
|
||||
const alphabeticallySortedTeamEnvironments = computed(() =>
|
||||
sortTeamEnvironmentsAlphabetically(props.teamEnvironments, "asc")
|
||||
)
|
||||
// Sort environments alphabetically by default and filter by search text
|
||||
const filteredAndAlphabetizedTeamEnvs = computed(() => {
|
||||
const envs = sortTeamEnvironmentsAlphabetically(props.teamEnvironments, "asc")
|
||||
const rawFilter = filterText.value
|
||||
|
||||
// Ensure specifying whitespace characters alone result in the empty state for no search results
|
||||
const trimmedFilter = rawFilter.trim().toLowerCase()
|
||||
|
||||
// Whitespace-only input results in an empty state
|
||||
if (rawFilter && !trimmedFilter) return []
|
||||
|
||||
// No search text → Show all environments
|
||||
if (!trimmedFilter) return envs
|
||||
|
||||
// Filter environments based on search text
|
||||
return envs.filter(({ env }) =>
|
||||
env.environment.name.toLowerCase().includes(trimmedFilter)
|
||||
)
|
||||
})
|
||||
|
||||
const showModalImportExport = ref(false)
|
||||
const showModalDetails = ref(false)
|
||||
|
|
@ -241,7 +277,7 @@ defineActionHandler(
|
|||
"modals.team.environment.edit",
|
||||
({ envName, variableName, isSecret }) => {
|
||||
if (variableName) editingVariableName.value = variableName
|
||||
const teamEnvToEdit = alphabeticallySortedTeamEnvironments.value.find(
|
||||
const teamEnvToEdit = filteredAndAlphabetizedTeamEnvs.value.find(
|
||||
({ env }) => env.environment.name === envName
|
||||
)
|
||||
if (teamEnvToEdit) {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,12 @@ import {
|
|||
keymap,
|
||||
tooltips,
|
||||
} from "@codemirror/view"
|
||||
import { EditorSelection, EditorState, Extension } from "@codemirror/state"
|
||||
import {
|
||||
Compartment,
|
||||
EditorSelection,
|
||||
EditorState,
|
||||
Extension,
|
||||
} from "@codemirror/state"
|
||||
import { clone } from "lodash-es"
|
||||
import { history, historyKeymap } from "@codemirror/commands"
|
||||
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||
|
|
@ -156,6 +161,9 @@ const isSecret = ref(props.secret)
|
|||
|
||||
const secretText = ref(props.modelValue)
|
||||
|
||||
// Compartment to store the readOnly state of the editor
|
||||
const readOnly = new Compartment()
|
||||
|
||||
watch(
|
||||
() => secretText.value,
|
||||
(newVal) => {
|
||||
|
|
@ -478,16 +486,14 @@ const initView = (el: any) => {
|
|||
}
|
||||
|
||||
const getExtensions = (readonly: boolean): Extension => {
|
||||
const extensions: Extension = [
|
||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
const readOnlyConfigs = [
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (readonly) {
|
||||
update.view.contentDOM.inputMode = "none"
|
||||
update.view.contentDOM.contentEditable = "false"
|
||||
}
|
||||
}),
|
||||
EditorState.changeFilter.of(() => !readonly),
|
||||
inputTheme,
|
||||
readonly
|
||||
? EditorView.theme({
|
||||
".cm-content": {
|
||||
|
|
@ -498,6 +504,13 @@ const getExtensions = (readonly: boolean): Extension => {
|
|||
},
|
||||
})
|
||||
: EditorView.theme({}),
|
||||
]
|
||||
|
||||
const extensions: Extension = [
|
||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
inputTheme,
|
||||
readOnly.of(readOnlyConfigs),
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
position: "absolute",
|
||||
|
|
@ -529,7 +542,8 @@ const getExtensions = (readonly: boolean): Extension => {
|
|||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
if (readonly) return
|
||||
// since the readonly prop is reactive, we need to check if it has changed
|
||||
if (props.readonly) return
|
||||
|
||||
if (update.docChanged) {
|
||||
const prevValue = clone(cachedValue.value)
|
||||
|
|
@ -610,6 +624,51 @@ watch(editor, () => {
|
|||
view.value = undefined
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Watch for changes in the readonly prop
|
||||
* and update the editor state accordingly
|
||||
*/
|
||||
watch(
|
||||
() => props.readonly,
|
||||
(isReadOnly) => {
|
||||
if (isReadOnly) {
|
||||
view.value?.dispatch({
|
||||
effects: readOnly.reconfigure([
|
||||
EditorView.theme({
|
||||
".cm-content": {
|
||||
caretColor: "var(--secondary-dark-color)",
|
||||
color: "var(--secondary-dark-color)",
|
||||
backgroundColor: "var(--divider-color)",
|
||||
opacity: 0.25,
|
||||
},
|
||||
}),
|
||||
EditorState.changeFilter.of(() => false),
|
||||
EditorState.readOnly.of(true),
|
||||
]),
|
||||
})
|
||||
|
||||
//change input mode and contenteditable to false to prevent keyboard input
|
||||
view.value?.contentDOM.setAttribute("inputmode", "none")
|
||||
view.value?.contentDOM.setAttribute("contenteditable", "false")
|
||||
} else {
|
||||
view.value?.dispatch({
|
||||
effects: readOnly.reconfigure([
|
||||
EditorView.theme({}),
|
||||
EditorState.changeFilter.of(() => true),
|
||||
EditorState.readOnly.of(false),
|
||||
]),
|
||||
})
|
||||
|
||||
//change input mode and contenteditable to true to allow keyboard input
|
||||
view.value?.contentDOM.setAttribute("inputmode", "text")
|
||||
view.value?.contentDOM.setAttribute("contenteditable", "true")
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
Loading…
Reference in a new issue