mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 10:11:37 +03:00
Only Escape cancels edits (#9913)
This commit is contained in:
parent
4376a5a851
commit
a14a95c057
10
README.md
10
README.md
@ -207,11 +207,11 @@ Enso consists of several sub projects:
|
||||
command line tools.
|
||||
|
||||
- **Enso IDE:** The
|
||||
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a desktop
|
||||
application that allows working with the visual form of Enso. It consists of
|
||||
an Electron application, a high performance WebGL UI framework, and the
|
||||
searcher which provides contextual search, hints, and documentation for all of
|
||||
Enso's functionality.
|
||||
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a
|
||||
desktop application that allows working with the visual form of Enso. It
|
||||
consists of an Electron application, a high performance WebGL UI framework,
|
||||
and the searcher which provides contextual search, hints, and documentation
|
||||
for all of Enso's functionality.
|
||||
|
||||
<br/>
|
||||
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
rangesForInputs,
|
||||
} from '@/components/ColorRing/gradient'
|
||||
import { injectInteractionHandler } from '@/providers/interactionHandler'
|
||||
import { targetIsOutside } from '@/util/autoBlur'
|
||||
import { endOnClickOutside } from '@/util/autoBlur'
|
||||
import { cssSupported, ensoColor, formatCssColor, parseCssColor } from '@/util/colors'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
@ -49,13 +49,12 @@ const svgElement = ref<HTMLElement>()
|
||||
const interaction = injectInteractionHandler()
|
||||
|
||||
onMounted(() => {
|
||||
interaction.setCurrent({
|
||||
cancel: () => emit('close'),
|
||||
pointerdown: (e: PointerEvent) => {
|
||||
if (targetIsOutside(e, svgElement.value)) emit('close')
|
||||
return false
|
||||
},
|
||||
})
|
||||
interaction.setCurrent(
|
||||
endOnClickOutside(svgElement, {
|
||||
cancel: () => emit('close'),
|
||||
end: () => emit('close'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const mouseSelectedAngle = ref<number>()
|
||||
|
@ -18,7 +18,7 @@ import { useProjectStore } from '@/stores/project'
|
||||
import { groupColorStyle, useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||
import { SuggestionKind } from '@/stores/suggestionDatabase/entry'
|
||||
import type { VisualizationDataSource } from '@/stores/visualization'
|
||||
import { targetIsOutside } from '@/util/autoBlur'
|
||||
import { endOnClickOutside } from '@/util/autoBlur'
|
||||
import { tryGetIndex } from '@/util/data/array'
|
||||
import type { Opt } from '@/util/data/opt'
|
||||
import { allRanges } from '@/util/data/range'
|
||||
@ -63,22 +63,19 @@ const emit = defineEmits<{
|
||||
canceled: []
|
||||
}>()
|
||||
|
||||
const cbOpen: Interaction = {
|
||||
cancel: () => {
|
||||
emit('canceled')
|
||||
},
|
||||
pointerdown: (e: PointerEvent) => {
|
||||
if (targetIsOutside(e, cbRoot.value)) {
|
||||
// In AI prompt mode likely the input is not a valid mode.
|
||||
if (input.anyChange.value && input.context.value.type !== 'aiPrompt') {
|
||||
acceptInput()
|
||||
} else {
|
||||
interaction.cancel(cbOpen)
|
||||
}
|
||||
const cbRoot = ref<HTMLElement>()
|
||||
|
||||
const cbOpen: Interaction = endOnClickOutside(cbRoot, {
|
||||
cancel: () => emit('canceled'),
|
||||
end: () => {
|
||||
// In AI prompt mode likely the input is not a valid mode.
|
||||
if (input.anyChange.value && input.context.value.type !== 'aiPrompt') {
|
||||
acceptInput()
|
||||
} else {
|
||||
emit('canceled')
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function scaleValues<T extends Record<any, number>>(
|
||||
values: T,
|
||||
@ -141,7 +138,6 @@ const transform = computed(() => {
|
||||
|
||||
// === Input and Filtering ===
|
||||
|
||||
const cbRoot = ref<HTMLElement>()
|
||||
const inputField = ref<HTMLInputElement>()
|
||||
const input = useComponentBrowserInput()
|
||||
const filterFlags = ref({ showUnstable: false, showLocal: false })
|
||||
@ -413,7 +409,7 @@ function acceptSuggestion(component: Opt<Component> = null) {
|
||||
|
||||
function acceptInput() {
|
||||
emit('accepted', input.code.value.trim(), input.importsToAdd())
|
||||
interaction.end(cbOpen)
|
||||
interaction.ended(cbOpen)
|
||||
}
|
||||
|
||||
// === Key Events Handler ===
|
||||
|
@ -25,13 +25,11 @@ const emits = defineEmits<{
|
||||
const MIN_DRAG_MOVE = 10
|
||||
|
||||
const editingEdge: Interaction = {
|
||||
cancel() {
|
||||
graph.clearUnconnected()
|
||||
},
|
||||
pointerdown(_e: PointerEvent, graphNavigator: GraphNavigator): boolean {
|
||||
return edgeInteractionClick(graphNavigator)
|
||||
},
|
||||
pointerup(e: PointerEvent, graphNavigator: GraphNavigator): boolean {
|
||||
cancel: () => graph.clearUnconnected(),
|
||||
end: () => graph.clearUnconnected(),
|
||||
pointerdown: (_e: PointerEvent, graphNavigator: GraphNavigator) =>
|
||||
edgeInteractionClick(graphNavigator),
|
||||
pointerup: (e: PointerEvent, graphNavigator: GraphNavigator) => {
|
||||
const originEvent = graph.unconnectedEdge?.event
|
||||
if (originEvent?.type === 'pointerdown') {
|
||||
const delta = new Vec2(e.screenX, e.screenY).sub(
|
||||
|
@ -26,6 +26,7 @@ const editor = ref<EditorViewType>()
|
||||
const interactions = injectInteractionHandler()
|
||||
const editInteraction = {
|
||||
cancel: () => finishEdit(),
|
||||
end: () => finishEdit(),
|
||||
click: (e: Event) => {
|
||||
if (e.target instanceof Element && !commentRoot.value?.contains(e.target)) finishEdit()
|
||||
return false
|
||||
|
@ -226,6 +226,7 @@ provideSelectionArrow(
|
||||
const isMulti = computed(() => props.input.dynamicConfig?.kind === 'Multiple_Choice')
|
||||
const dropDownInteraction = WidgetEditHandler.New('WidgetSelection', props.input, {
|
||||
cancel: () => {},
|
||||
end: () => {},
|
||||
pointerdown: (e, _) => {
|
||||
if (targetIsOutside(e, unrefElement(dropdownElement))) {
|
||||
dropDownInteraction.end()
|
||||
|
@ -28,7 +28,7 @@ export class InteractionHandler {
|
||||
|
||||
setCurrent(interaction: Interaction | undefined) {
|
||||
if (!this.isActive(interaction)) {
|
||||
this.currentInteraction?.cancel?.()
|
||||
this.currentInteraction?.end()
|
||||
this.currentInteraction = interaction
|
||||
}
|
||||
}
|
||||
@ -37,19 +37,31 @@ export class InteractionHandler {
|
||||
return this.currentInteraction
|
||||
}
|
||||
|
||||
/** Unset the current interaction, if it is the specified instance. */
|
||||
end(interaction: Interaction) {
|
||||
/** Clear the current interaction without calling any callback, if the current interaction is `interaction`. */
|
||||
ended(interaction: Interaction) {
|
||||
if (this.isActive(interaction)) this.currentInteraction = undefined
|
||||
}
|
||||
|
||||
/** End the current interaction, if it is the specified instance. */
|
||||
end(interaction: Interaction) {
|
||||
if (this.isActive(interaction)) {
|
||||
this.currentInteraction = undefined
|
||||
interaction.end()
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancel the current interaction, if it is the specified instance. */
|
||||
cancel(interaction: Interaction) {
|
||||
if (this.isActive(interaction)) this.setCurrent(undefined)
|
||||
if (this.isActive(interaction)) {
|
||||
this.currentInteraction = undefined
|
||||
interaction.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
handleCancel(): boolean {
|
||||
const hasCurrent = this.currentInteraction != null
|
||||
if (hasCurrent) this.setCurrent(undefined)
|
||||
this.currentInteraction?.cancel()
|
||||
this.currentInteraction = undefined
|
||||
return hasCurrent
|
||||
}
|
||||
|
||||
@ -74,7 +86,10 @@ export class InteractionHandler {
|
||||
type InteractionEventHandler = (event: PointerEvent, navigator: GraphNavigator) => boolean | void
|
||||
|
||||
export interface Interaction {
|
||||
/** Called when the interaction is explicitly canceled, e.g. with the `Esc` key. */
|
||||
cancel(): void
|
||||
/** Called when the interaction is ended due to activity elsewhere. */
|
||||
end(): void
|
||||
/** Uses a `capture` event handler to allow an interaction to respond to clicks over any element. */
|
||||
pointerdown?: InteractionEventHandler
|
||||
/** Uses a `capture` event handler to allow an interaction to respond to mouse button release
|
||||
|
@ -112,7 +112,7 @@ test.each`
|
||||
expect(editedHandler.handler.isActive()).toBeTruthy()
|
||||
interactionHandler.setCurrent(undefined)
|
||||
expect(widgetTree.currentEdit).toBeUndefined()
|
||||
checkCallbackCall('cancel')
|
||||
checkCallbackCall('end', undefined)
|
||||
expect(editedHandler.handler.isActive()).toBeFalsy()
|
||||
},
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ export class WidgetEditHandler {
|
||||
noLongerActive()
|
||||
hooks.cancel?.()
|
||||
},
|
||||
end: (origin: WidgetId) => {
|
||||
end: (origin?: WidgetId) => {
|
||||
noLongerActive()
|
||||
hooks.end?.(origin)
|
||||
},
|
||||
@ -151,7 +151,7 @@ export interface WidgetEditHooks extends Interaction {
|
||||
* {@link WidgetEditHandler} being called, or because a child is to be started.
|
||||
*/
|
||||
start?(origin: WidgetId): void
|
||||
end?(origin: WidgetId): void
|
||||
end(origin?: WidgetId | undefined): void
|
||||
/**
|
||||
* Hook called when a child widget, or this widget itself, provides an updated value.
|
||||
*/
|
||||
@ -223,7 +223,7 @@ class PortEditInteraction implements Interaction {
|
||||
this.shutdown()
|
||||
}
|
||||
|
||||
end(origin: WidgetId) {
|
||||
end(origin?: WidgetId) {
|
||||
for (const interaction of this.interactions) interaction.end?.(origin)
|
||||
this.shutdown()
|
||||
}
|
||||
@ -231,7 +231,7 @@ class PortEditInteraction implements Interaction {
|
||||
private shutdown() {
|
||||
this.interactions.length = 0
|
||||
this.active.value = false
|
||||
this.interactionHandler.end(this)
|
||||
this.interactionHandler.ended(this)
|
||||
}
|
||||
|
||||
register(interaction: PortEditSubinteraction) {
|
||||
@ -269,6 +269,8 @@ class SuspendedPortEdit implements Interaction {
|
||||
}
|
||||
|
||||
cancel() {}
|
||||
|
||||
end() {}
|
||||
}
|
||||
|
||||
/** A sub-interaction of a @{link PortEditInteraction} */
|
||||
@ -276,7 +278,8 @@ interface PortEditSubinteraction extends Interaction {
|
||||
widgetId: WidgetId
|
||||
|
||||
suspend?: () => { resume: () => void }
|
||||
end?(origin: WidgetId): void
|
||||
|
||||
end(origin?: WidgetId | undefined): void
|
||||
}
|
||||
|
||||
/** @internal Public for unit testing.
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { useEvent } from '@/composables/events'
|
||||
import { unrefElement, useEvent } from '@/composables/events'
|
||||
import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler'
|
||||
import type { VueInstance } from '@vueuse/core'
|
||||
import type { Opt } from 'shared/util/data/opt'
|
||||
import { watchEffect, type Ref } from 'vue'
|
||||
|
||||
@ -45,3 +47,24 @@ export function registerAutoBlurHandler() {
|
||||
export function targetIsOutside(e: Event, area: Opt<Element>): boolean {
|
||||
return !!area && e.target instanceof Element && !area.contains(e.target)
|
||||
}
|
||||
|
||||
/** Returns a new interaction based on the given `interaction`. The new interaction will be ended if a pointerdown event
|
||||
* occurs outside the given `area` element. */
|
||||
export function endOnClickOutside(
|
||||
area: Ref<Element | VueInstance | null | undefined>,
|
||||
interaction: Interaction,
|
||||
): Interaction {
|
||||
const chainedPointerdown = interaction.pointerdown
|
||||
const handler = injectInteractionHandler()
|
||||
const wrappedInteraction: Interaction = {
|
||||
...interaction,
|
||||
pointerdown: (e: PointerEvent, ...args) => {
|
||||
if (targetIsOutside(e, unrefElement(area))) {
|
||||
handler.end(wrappedInteraction)
|
||||
return false
|
||||
}
|
||||
return chainedPointerdown ? chainedPointerdown(e, ...args) : false
|
||||
},
|
||||
}
|
||||
return wrappedInteraction
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user