mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 03:51:43 +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;
|
||||
--background-color: #eaeaea;
|
||||
--doc-panel-bottom-clip: 4px;
|
||||
width: 295px;
|
||||
min-width: 295px;
|
||||
width: min-content;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
font-size: 11.5px;
|
||||
display: flex;
|
||||
|
@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
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 { Icon } from '@/util/iconName'
|
||||
import { computed, ref, watch, type DeepReadonly } from 'vue'
|
||||
|
||||
type Range = { start: number; end: number }
|
||||
import { ComponentExposed } from 'vue-component-type-helpers'
|
||||
|
||||
const content = defineModel<DeepReadonly<{ text: string; selection: Range | undefined }>>({
|
||||
required: true,
|
||||
@ -16,52 +15,25 @@ const props = defineProps<{
|
||||
nodeColor: string
|
||||
}>()
|
||||
|
||||
const inputField = ref<HTMLInputElement>()
|
||||
const fieldText = ref<string>('')
|
||||
const fieldSelection = ref<Range>()
|
||||
const inputField = ref<ComponentExposed<typeof AutoSizedInput>>()
|
||||
|
||||
watch(content, ({ text: newText, selection: newPos }) => {
|
||||
fieldText.value = newText
|
||||
if (inputField.value == null) return
|
||||
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)
|
||||
const fieldContent = ref<{ text: string; selection: Range | undefined }>({
|
||||
text: '',
|
||||
selection: undefined,
|
||||
})
|
||||
|
||||
watch(fieldText, readInputFieldSelection)
|
||||
|
||||
watch([fieldText, fieldSelection], ([newText, newSelection]) => {
|
||||
content.value = {
|
||||
text: newText,
|
||||
selection: newSelection,
|
||||
}
|
||||
watch(content, (newContent) => {
|
||||
fieldContent.value = newContent
|
||||
})
|
||||
|
||||
function readInputFieldSelection() {
|
||||
if (
|
||||
inputField.value != null &&
|
||||
inputField.value.selectionStart != null &&
|
||||
inputField.value.selectionEnd != null
|
||||
) {
|
||||
fieldSelection.value = {
|
||||
start: inputField.value.selectionStart,
|
||||
end: inputField.value.selectionEnd,
|
||||
watch(
|
||||
[() => fieldContent.value.text, () => fieldContent.value.selection],
|
||||
([newText, newSelection]) => {
|
||||
content.value = {
|
||||
text: newText,
|
||||
selection: newSelection,
|
||||
}
|
||||
} 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({
|
||||
blur: () => inputField.value?.blur(),
|
||||
@ -81,11 +53,13 @@ const rootStyle = computed(() => {
|
||||
<div v-if="props.icon" class="iconPort">
|
||||
<SvgIcon :name="props.icon" class="nodeIcon" />
|
||||
</div>
|
||||
<input
|
||||
<AutoSizedInput
|
||||
ref="inputField"
|
||||
v-model="fieldText"
|
||||
v-model="fieldContent.text"
|
||||
v-model:selection="fieldContent.selection"
|
||||
autocomplete="off"
|
||||
class="inputField"
|
||||
:acceptOnEnter="false"
|
||||
@pointerdown.stop
|
||||
@pointerup.stop
|
||||
@click.stop
|
||||
@ -105,20 +79,20 @@ const rootStyle = computed(() => {
|
||||
border-radius: var(--radius-default);
|
||||
background-color: var(--background-color);
|
||||
padding: 0 var(--component-editor-padding);
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
border: none;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
background: none;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.iconPort {
|
||||
@ -151,8 +125,7 @@ const rootStyle = computed(() => {
|
||||
.buttonPanel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
gap: 8px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
</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 { qnLastSegment, type QualifiedName } from '@/util/qualifiedName'
|
||||
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. */
|
||||
export type Usage =
|
||||
|
@ -1,12 +1,23 @@
|
||||
<script lang="ts">
|
||||
export type Range = { start: number; end: number }
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEvent } from '@/composables/events'
|
||||
import { useAutoBlur } from '@/util/autoBlur'
|
||||
import { getTextWidthByFont } from '@/util/measurement'
|
||||
import { computed, ref, watch, type StyleValue } from 'vue'
|
||||
|
||||
const [model, modifiers] = defineModel<string>()
|
||||
const props = defineProps<{
|
||||
const [selection] = defineModel<Range | undefined>('selection')
|
||||
const {
|
||||
autoSelect = false,
|
||||
placeholder = '',
|
||||
acceptOnEnter = true,
|
||||
} = defineProps<{
|
||||
autoSelect?: boolean
|
||||
placeholder?: string | undefined
|
||||
acceptOnEnter?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
input: [value: string | undefined]
|
||||
@ -20,10 +31,15 @@ function onChange() {
|
||||
emit('change', innerModel.value)
|
||||
}
|
||||
|
||||
function onInput() {
|
||||
readInputFieldSelection()
|
||||
emit('input', innerModel.value)
|
||||
}
|
||||
|
||||
const inputNode = ref<HTMLInputElement>()
|
||||
useAutoBlur(inputNode)
|
||||
function onFocus() {
|
||||
if (props.autoSelect) {
|
||||
if (autoSelect) {
|
||||
inputNode.value?.select()
|
||||
}
|
||||
}
|
||||
@ -38,15 +54,44 @@ const cssFont = computed(() => {
|
||||
const ADDED_WIDTH_PX = 2
|
||||
|
||||
const getTextWidth = (text: string) => getTextWidthByFont(text, cssFont.value)
|
||||
const inputWidth = computed(
|
||||
() => getTextWidth(innerModel.value || (props.placeholder ?? '')) + ADDED_WIDTH_PX,
|
||||
)
|
||||
const inputWidth = computed(() => getTextWidth(innerModel.value || placeholder) + ADDED_WIDTH_PX)
|
||||
const inputStyle = computed<StyleValue>(() => ({ width: `${inputWidth.value}px` }))
|
||||
|
||||
function onEnterDown() {
|
||||
inputNode.value?.blur()
|
||||
function onEnterDown(event: KeyboardEvent) {
|
||||
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({
|
||||
inputWidth,
|
||||
getTextWidth,
|
||||
@ -65,7 +110,7 @@ defineExpose({
|
||||
ref="inputNode"
|
||||
v-model="innerModel"
|
||||
class="AutoSizedInput input"
|
||||
:placeholder="placeholder ?? ''"
|
||||
:placeholder="placeholder"
|
||||
:style="inputStyle"
|
||||
@pointerdown.stop
|
||||
@click.stop
|
||||
@ -73,8 +118,8 @@ defineExpose({
|
||||
@keydown.delete.stop
|
||||
@keydown.arrow-left.stop
|
||||
@keydown.arrow-right.stop
|
||||
@keydown.enter.stop="onEnterDown"
|
||||
@input="emit('input', innerModel)"
|
||||
@keydown.enter="onEnterDown"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
@focus="onFocus"
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user