mirror of
https://github.com/enso-org/enso.git
synced 2025-01-09 03:57:54 +03:00
Autosized Component Browser Input (#11161)
Closes #11011 When the code in Component Browser input is longer than the initial width, the CB is extended. [Screencast From 2024-09-24 14-13-51.webm](https://github.com/user-attachments/assets/ae9df4e2-dcc8-46aa-9977-32f470fd85fd)
This commit is contained in:
parent
0f4fa42eea
commit
79d40820cc
@ -434,7 +434,8 @@ const handler = componentBrowserBindings.handler({
|
|||||||
--radius-default: 20px;
|
--radius-default: 20px;
|
||||||
--background-color: #eaeaea;
|
--background-color: #eaeaea;
|
||||||
--doc-panel-bottom-clip: 4px;
|
--doc-panel-bottom-clip: 4px;
|
||||||
width: 295px;
|
min-width: 295px;
|
||||||
|
width: min-content;
|
||||||
color: rgba(0, 0, 0, 0.6);
|
color: rgba(0, 0, 0, 0.6);
|
||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SvgIcon from '@/components/SvgIcon.vue'
|
import SvgIcon from '@/components/SvgIcon.vue'
|
||||||
import { useEvent } from '@/composables/events'
|
import AutoSizedInput, { type Range } from '@/components/widgets/AutoSizedInput.vue'
|
||||||
import type { useNavigator } from '@/composables/navigator'
|
import type { useNavigator } from '@/composables/navigator'
|
||||||
import type { Icon } from '@/util/iconName'
|
import type { Icon } from '@/util/iconName'
|
||||||
import { computed, ref, watch, type DeepReadonly } from 'vue'
|
import { computed, ref, watch, type DeepReadonly } from 'vue'
|
||||||
|
import { ComponentExposed } from 'vue-component-type-helpers'
|
||||||
type Range = { start: number; end: number }
|
|
||||||
|
|
||||||
const content = defineModel<DeepReadonly<{ text: string; selection: Range | undefined }>>({
|
const content = defineModel<DeepReadonly<{ text: string; selection: Range | undefined }>>({
|
||||||
required: true,
|
required: true,
|
||||||
@ -16,52 +15,25 @@ const props = defineProps<{
|
|||||||
nodeColor: string
|
nodeColor: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const inputField = ref<HTMLInputElement>()
|
const inputField = ref<ComponentExposed<typeof AutoSizedInput>>()
|
||||||
const fieldText = ref<string>('')
|
|
||||||
const fieldSelection = ref<Range>()
|
|
||||||
|
|
||||||
watch(content, ({ text: newText, selection: newPos }) => {
|
const fieldContent = ref<{ text: string; selection: Range | undefined }>({
|
||||||
fieldText.value = newText
|
text: '',
|
||||||
if (inputField.value == null) return
|
selection: undefined,
|
||||||
inputField.value.value = newText
|
|
||||||
// If boundaries didn't change, don't overwrite selection dir.
|
|
||||||
if (
|
|
||||||
inputField.value.selectionStart !== newPos?.start ||
|
|
||||||
inputField.value.selectionEnd !== newPos?.end
|
|
||||||
)
|
|
||||||
inputField.value.setSelectionRange(newPos?.start ?? null, newPos?.end ?? null)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(fieldText, readInputFieldSelection)
|
watch(content, (newContent) => {
|
||||||
|
fieldContent.value = newContent
|
||||||
watch([fieldText, fieldSelection], ([newText, newSelection]) => {
|
|
||||||
content.value = {
|
|
||||||
text: newText,
|
|
||||||
selection: newSelection,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
watch(
|
||||||
function readInputFieldSelection() {
|
[() => fieldContent.value.text, () => fieldContent.value.selection],
|
||||||
if (
|
([newText, newSelection]) => {
|
||||||
inputField.value != null &&
|
content.value = {
|
||||||
inputField.value.selectionStart != null &&
|
text: newText,
|
||||||
inputField.value.selectionEnd != null
|
selection: newSelection,
|
||||||
) {
|
|
||||||
fieldSelection.value = {
|
|
||||||
start: inputField.value.selectionStart,
|
|
||||||
end: inputField.value.selectionEnd,
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
fieldSelection.value = undefined
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTMLInputElement's same event is not supported in chrome yet. We just react for any
|
|
||||||
// selectionchange in the document and check if the input selection changed.
|
|
||||||
// BUT some operations like deleting does not emit 'selectionChange':
|
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=725890
|
|
||||||
// Therefore we must also refresh selection after changing input.
|
|
||||||
useEvent(document, 'selectionchange', readInputFieldSelection)
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
blur: () => inputField.value?.blur(),
|
blur: () => inputField.value?.blur(),
|
||||||
@ -81,11 +53,13 @@ const rootStyle = computed(() => {
|
|||||||
<div v-if="props.icon" class="iconPort">
|
<div v-if="props.icon" class="iconPort">
|
||||||
<SvgIcon :name="props.icon" class="nodeIcon" />
|
<SvgIcon :name="props.icon" class="nodeIcon" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<AutoSizedInput
|
||||||
ref="inputField"
|
ref="inputField"
|
||||||
v-model="fieldText"
|
v-model="fieldContent.text"
|
||||||
|
v-model:selection="fieldContent.selection"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="inputField"
|
class="inputField"
|
||||||
|
:acceptOnEnter="false"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@pointerup.stop
|
@pointerup.stop
|
||||||
@click.stop
|
@click.stop
|
||||||
@ -105,20 +79,20 @@ const rootStyle = computed(() => {
|
|||||||
border-radius: var(--radius-default);
|
border-radius: var(--radius-default);
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
padding: 0 var(--component-editor-padding);
|
padding: 0 var(--component-editor-padding);
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputField {
|
.inputField {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
min-width: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
background: none;
|
background: none;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconPort {
|
.iconPort {
|
||||||
@ -151,8 +125,7 @@ const rootStyle = computed(() => {
|
|||||||
.buttonPanel {
|
.buttonPanel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-shrink: 0;
|
|
||||||
flex-grow: 0;
|
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -9,7 +9,7 @@ import { isIdentifier, type AstId, type Identifier } from '@/util/ast/abstract'
|
|||||||
import { Err, Ok, type Result } from '@/util/data/result'
|
import { Err, Ok, type Result } from '@/util/data/result'
|
||||||
import { qnLastSegment, type QualifiedName } from '@/util/qualifiedName'
|
import { qnLastSegment, type QualifiedName } from '@/util/qualifiedName'
|
||||||
import { useToast } from '@/util/toast'
|
import { useToast } from '@/util/toast'
|
||||||
import { computed, proxyRefs, readonly, ref, type ComputedRef } from 'vue'
|
import { computed, proxyRefs, readonly, ref, watch, type ComputedRef } from 'vue'
|
||||||
|
|
||||||
/** Information how the component browser is used, needed for proper input initializing. */
|
/** Information how the component browser is used, needed for proper input initializing. */
|
||||||
export type Usage =
|
export type Usage =
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export type Range = { start: number; end: number }
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useEvent } from '@/composables/events'
|
||||||
import { useAutoBlur } from '@/util/autoBlur'
|
import { useAutoBlur } from '@/util/autoBlur'
|
||||||
import { getTextWidthByFont } from '@/util/measurement'
|
import { getTextWidthByFont } from '@/util/measurement'
|
||||||
import { computed, ref, watch, type StyleValue } from 'vue'
|
import { computed, ref, watch, type StyleValue } from 'vue'
|
||||||
|
|
||||||
const [model, modifiers] = defineModel<string>()
|
const [model, modifiers] = defineModel<string>()
|
||||||
const props = defineProps<{
|
const [selection] = defineModel<Range | undefined>('selection')
|
||||||
|
const {
|
||||||
|
autoSelect = false,
|
||||||
|
placeholder = '',
|
||||||
|
acceptOnEnter = true,
|
||||||
|
} = defineProps<{
|
||||||
autoSelect?: boolean
|
autoSelect?: boolean
|
||||||
placeholder?: string | undefined
|
placeholder?: string | undefined
|
||||||
|
acceptOnEnter?: boolean
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
input: [value: string | undefined]
|
input: [value: string | undefined]
|
||||||
@ -20,10 +31,15 @@ function onChange() {
|
|||||||
emit('change', innerModel.value)
|
emit('change', innerModel.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onInput() {
|
||||||
|
readInputFieldSelection()
|
||||||
|
emit('input', innerModel.value)
|
||||||
|
}
|
||||||
|
|
||||||
const inputNode = ref<HTMLInputElement>()
|
const inputNode = ref<HTMLInputElement>()
|
||||||
useAutoBlur(inputNode)
|
useAutoBlur(inputNode)
|
||||||
function onFocus() {
|
function onFocus() {
|
||||||
if (props.autoSelect) {
|
if (autoSelect) {
|
||||||
inputNode.value?.select()
|
inputNode.value?.select()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,15 +54,44 @@ const cssFont = computed(() => {
|
|||||||
const ADDED_WIDTH_PX = 2
|
const ADDED_WIDTH_PX = 2
|
||||||
|
|
||||||
const getTextWidth = (text: string) => getTextWidthByFont(text, cssFont.value)
|
const getTextWidth = (text: string) => getTextWidthByFont(text, cssFont.value)
|
||||||
const inputWidth = computed(
|
const inputWidth = computed(() => getTextWidth(innerModel.value || placeholder) + ADDED_WIDTH_PX)
|
||||||
() => getTextWidth(innerModel.value || (props.placeholder ?? '')) + ADDED_WIDTH_PX,
|
|
||||||
)
|
|
||||||
const inputStyle = computed<StyleValue>(() => ({ width: `${inputWidth.value}px` }))
|
const inputStyle = computed<StyleValue>(() => ({ width: `${inputWidth.value}px` }))
|
||||||
|
|
||||||
function onEnterDown() {
|
function onEnterDown(event: KeyboardEvent) {
|
||||||
inputNode.value?.blur()
|
if (acceptOnEnter) {
|
||||||
|
event.stopPropagation()
|
||||||
|
inputNode.value?.blur()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readInputFieldSelection() {
|
||||||
|
if (inputNode.value?.selectionStart != null && inputNode.value.selectionEnd != null) {
|
||||||
|
selection.value = {
|
||||||
|
start: inputNode.value.selectionStart,
|
||||||
|
end: inputNode.value.selectionEnd,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selection.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLInputElement's same event is not supported in chrome yet. We just react for any
|
||||||
|
// selectionchange in the document and check if the input selection changed.
|
||||||
|
// BUT some operations like deleting does not emit 'selectionChange':
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=725890
|
||||||
|
// Therefore we must also refresh selection after changing input.
|
||||||
|
useEvent(document, 'selectionchange', readInputFieldSelection)
|
||||||
|
|
||||||
|
watch(selection, (newPos) => {
|
||||||
|
// If boundaries didn't change, don't overwrite selection dir.
|
||||||
|
if (
|
||||||
|
inputNode.value?.selectionStart !== newPos?.start ||
|
||||||
|
inputNode.value?.selectionEnd !== newPos?.end
|
||||||
|
) {
|
||||||
|
inputNode.value?.setSelectionRange(newPos?.start ?? null, newPos?.end ?? null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
inputWidth,
|
inputWidth,
|
||||||
getTextWidth,
|
getTextWidth,
|
||||||
@ -65,7 +110,7 @@ defineExpose({
|
|||||||
ref="inputNode"
|
ref="inputNode"
|
||||||
v-model="innerModel"
|
v-model="innerModel"
|
||||||
class="AutoSizedInput input"
|
class="AutoSizedInput input"
|
||||||
:placeholder="placeholder ?? ''"
|
:placeholder="placeholder"
|
||||||
:style="inputStyle"
|
:style="inputStyle"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@click.stop
|
@click.stop
|
||||||
@ -73,8 +118,8 @@ defineExpose({
|
|||||||
@keydown.delete.stop
|
@keydown.delete.stop
|
||||||
@keydown.arrow-left.stop
|
@keydown.arrow-left.stop
|
||||||
@keydown.arrow-right.stop
|
@keydown.arrow-right.stop
|
||||||
@keydown.enter.stop="onEnterDown"
|
@keydown.enter="onEnterDown"
|
||||||
@input="emit('input', innerModel)"
|
@input="onInput"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user