mirror of
https://github.com/enso-org/enso.git
synced 2024-11-05 03:59:38 +03:00
Format bar support APIs (#10063)
Introduce APIs that will be used by the Markdown formatting bar UI; use new APIs to simplify `ExtendedMenu`. # Important Notes New APIs: - `DropdownMenu`: Encapsulates the dropdown pattern, i.e. a toggle button that opens a floating pane. - `MenuButton`: A low-level control that adds button behavior to any element; all other buttons are now implemented using it. Changes: - `ToggleButton` and `SvgButton` now accept an optional `label` parameter.
This commit is contained in:
parent
a383819439
commit
ac5fbbcd17
81
app/gui2/src/components/DropdownMenu.vue
Normal file
81
app/gui2/src/components/DropdownMenu.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import MenuButton from '@/components/MenuButton.vue'
|
||||
import SizeTransition from '@/components/SizeTransition.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { injectInteractionHandler } from '@/providers/interactionHandler'
|
||||
import { endOnClickOutside } from '@/util/autoBlur'
|
||||
import { shift, useFloating, type Placement } from '@floating-ui/vue'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const props = defineProps<{
|
||||
placement?: Placement
|
||||
}>()
|
||||
|
||||
const rootElement = shallowRef<HTMLElement>()
|
||||
const floatElement = shallowRef<HTMLElement>()
|
||||
const hovered = ref(false)
|
||||
|
||||
injectInteractionHandler().setWhen(
|
||||
open,
|
||||
endOnClickOutside(rootElement, {
|
||||
cancel: () => (open.value = false),
|
||||
end: () => (open.value = false),
|
||||
}),
|
||||
)
|
||||
|
||||
const { floatingStyles } = useFloating(rootElement, floatElement, {
|
||||
placement: props.placement ?? 'bottom-start',
|
||||
middleware: [shift()],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rootElement" class="DropdownMenu" @pointerdown.prevent>
|
||||
<MenuButton
|
||||
v-model="open"
|
||||
class="DropdownMenuButton"
|
||||
@pointerenter="hovered = true"
|
||||
@pointerleave="hovered = false"
|
||||
>
|
||||
<slot name="button" />
|
||||
</MenuButton>
|
||||
<SvgIcon v-if="hovered && !open" name="arrow_right_head_only" class="arrow" />
|
||||
<SizeTransition height :duration="100">
|
||||
<div v-if="open" ref="floatElement" class="DropdownMenuContent" :style="floatingStyles">
|
||||
<slot name="entries" />
|
||||
</div>
|
||||
</SizeTransition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.DropdownMenu {
|
||||
position: relative;
|
||||
outline: 0;
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
.DropdownMenuContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 13px;
|
||||
background: var(--color-frame-bg);
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
margin: 0 -4px;
|
||||
z-index: 1;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(90deg) scale(0.7);
|
||||
transform-origin: center;
|
||||
opacity: 0.5;
|
||||
/* Prevent the parent from receiving a pointerout event if the mouse is over the arrow, which causes flickering. */
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { codeEditorBindings, documentationEditorBindings } from '@/bindings'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
import MenuButton from '@/components/MenuButton.vue'
|
||||
import SvgButton from '@/components/SvgButton.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler'
|
||||
import { targetIsOutside } from '@/util/autoBlur'
|
||||
import { ref } from 'vue'
|
||||
import SvgButton from './SvgButton.vue'
|
||||
|
||||
const showCodeEditor = defineModel<boolean>('showCodeEditor', { required: true })
|
||||
const showDocumentationEditor = defineModel<boolean>('showDocumentationEditor', { required: true })
|
||||
@ -17,48 +17,19 @@ const emit = defineEmits<{
|
||||
fitToAllClicked: []
|
||||
}>()
|
||||
|
||||
const buttonElement = ref<HTMLElement>()
|
||||
const menuElement = ref<HTMLElement>()
|
||||
|
||||
const isDropdownOpen = ref(false)
|
||||
|
||||
const interaction = injectInteractionHandler()
|
||||
const dropdownInteraction: Interaction = {
|
||||
cancel: () => (isDropdownOpen.value = false),
|
||||
end: () => (isDropdownOpen.value = false),
|
||||
pointerdown: (e: PointerEvent) => {
|
||||
if ([buttonElement.value, menuElement.value].every((area) => targetIsOutside(e, area)))
|
||||
closeDropdown()
|
||||
return false
|
||||
},
|
||||
}
|
||||
function openDropdown() {
|
||||
isDropdownOpen.value = true
|
||||
interaction.setCurrent(dropdownInteraction)
|
||||
}
|
||||
function closeDropdown() {
|
||||
interaction.cancel(dropdownInteraction)
|
||||
}
|
||||
function toggleDropdown() {
|
||||
isDropdownOpen.value ? closeDropdown() : openDropdown()
|
||||
}
|
||||
const open = ref(false)
|
||||
|
||||
const toggleCodeEditorShortcut = codeEditorBindings.bindings.toggle.humanReadable
|
||||
const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.toggle.humanReadable
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="buttonElement"
|
||||
class="ExtendedMenu"
|
||||
title="Additional Options"
|
||||
@click.stop="toggleDropdown"
|
||||
>
|
||||
<SvgIcon name="3_dot_menu" class="moreIcon" />
|
||||
</div>
|
||||
<Transition name="dropdown">
|
||||
<div v-show="isDropdownOpen" ref="menuElement" class="ExtendedMenuPane">
|
||||
<div class="row">
|
||||
<DropdownMenu v-model:open="open" placement="bottom-end" class="ExtendedMenu">
|
||||
<template #button
|
||||
><SvgIcon name="3_dot_menu" class="moreIcon" title="Additional Options"
|
||||
/></template>
|
||||
<template #entries>
|
||||
<div>
|
||||
<div class="label">Zoom</div>
|
||||
<div class="zoomControl">
|
||||
<SvgButton
|
||||
@ -81,67 +52,45 @@ const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.t
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="row clickableRow"
|
||||
:class="{ selected: showCodeEditor }"
|
||||
@click="(showCodeEditor = !showCodeEditor), closeDropdown()"
|
||||
>
|
||||
<div class="label">Code Editor</div>
|
||||
<MenuButton v-model="showCodeEditor" @click="open = false">
|
||||
Code Editor
|
||||
<div v-text="toggleCodeEditorShortcut" />
|
||||
</div>
|
||||
<div
|
||||
class="row clickableRow"
|
||||
:class="{ selected: showDocumentationEditor }"
|
||||
@click="(showDocumentationEditor = !showDocumentationEditor), closeDropdown()"
|
||||
>
|
||||
<div class="label">Documentation Editor</div>
|
||||
</MenuButton>
|
||||
<MenuButton v-model="showDocumentationEditor" @click="open = false">
|
||||
Documentation Editor
|
||||
<div v-text="toggleDocumentationEditorShortcut" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</MenuButton>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ExtendedMenu {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
gap: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: auto;
|
||||
margin-right: 125px;
|
||||
background: var(--color-frame-bg);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-frame-bg);
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
cursor: pointer;
|
||||
margin: 0 125px 0 auto;
|
||||
}
|
||||
|
||||
.ExtendedMenuPane {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.moreIcon {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.ExtendedMenu :deep(.DropdownMenuContent) {
|
||||
width: 250px;
|
||||
top: 40px;
|
||||
margin-top: 6px;
|
||||
margin-top: 2px;
|
||||
padding: 4px;
|
||||
right: 8px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-frame-bg);
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
|
||||
> * {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--color-menu-entry-hover-bg);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-menu-entry-active-bg);
|
||||
}
|
||||
&.selected {
|
||||
background-color: var(--color-menu-entry-selected-bg);
|
||||
}
|
||||
.toggledOn {
|
||||
background-color: var(--color-menu-entry-selected-bg);
|
||||
}
|
||||
|
||||
.label {
|
||||
@ -149,15 +98,6 @@ const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.t
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0 8px 0 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-left: 1px solid var(--color-text);
|
||||
border-right: 1px solid var(--color-text);
|
||||
@ -177,23 +117,4 @@ const toggleDocumentationEditorShortcut = documentationEditorBindings.bindings.t
|
||||
width: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.moreIcon {
|
||||
position: relative;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.zoomButton {
|
||||
--icon-transform: scale(12/16);
|
||||
}
|
||||
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
54
app/gui2/src/components/MenuButton.vue
Normal file
54
app/gui2/src/components/MenuButton.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* A button. Supports toggling and disabled state.
|
||||
*
|
||||
* If a boolean model is bound to the primary model, clicking the button will switch between `toggledOn` and
|
||||
* `toggledOff` css classes on the slot's root element, as well as updating the model.
|
||||
*
|
||||
* If the disabled property is set, the button stops responding to mouse interaction and its contents will have the
|
||||
* `disabled` class.
|
||||
*/
|
||||
|
||||
const toggledOn = defineModel<boolean>({ default: undefined })
|
||||
const props = defineProps<{ disabled?: boolean | undefined }>()
|
||||
|
||||
function onClick() {
|
||||
if (!props.disabled && toggledOn.value != null) toggledOn.value = !toggledOn.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="MenuButton button"
|
||||
:class="{ toggledOn, toggledOff: toggledOn === false, disabled }"
|
||||
:disabled="disabled ?? false"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.MenuButton {
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
min-width: max-content;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-full);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--color-menu-entry-hover-bg);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-menu-entry-active-bg);
|
||||
}
|
||||
&.disabled {
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,47 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import MenuButton from '@/components/MenuButton.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import type { URLString } from '@/util/data/urlString'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
|
||||
const _props = defineProps<{
|
||||
name: Icon | URLString
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="SvgButton button" :class="{ disabled }" :disabled="disabled ?? false">
|
||||
<SvgIcon :name="name" draggable="false" />
|
||||
</button>
|
||||
<MenuButton :disabled="disabled" class="SvgButton">
|
||||
<SvgIcon :name="name" />
|
||||
<div v-if="label" v-text="label" />
|
||||
</MenuButton>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.SvgButton {
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
margin: -4px -4px;
|
||||
border-radius: var(--radius-full);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-menu-entry-hover-bg);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
transform: var(--icon-transform);
|
||||
transform-origin: var(--icon-transform-origin);
|
||||
}
|
||||
margin: -4px;
|
||||
gap: 4px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -27,5 +27,7 @@ svg {
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
transform: var(--icon-transform);
|
||||
transform-origin: var(--icon-transform-origin);
|
||||
}
|
||||
</style>
|
||||
|
@ -6,27 +6,27 @@
|
||||
* element.
|
||||
*/
|
||||
|
||||
import SvgButton from '@/components/SvgButton.vue'
|
||||
import MenuButton from '@/components/MenuButton.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
|
||||
const _props = withDefaults(defineProps<{ icon: Icon; modelValue?: boolean }>(), {
|
||||
modelValue: false,
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [toggledOn: boolean]
|
||||
}>()
|
||||
const toggledOn = defineModel<boolean>({ default: false })
|
||||
const _props = defineProps<{ icon: Icon; label?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SvgButton
|
||||
:name="icon"
|
||||
class="ToggleIcon"
|
||||
:class="{ toggledOn: modelValue, toggledOff: !modelValue }"
|
||||
@click.stop="emit('update:modelValue', !modelValue)"
|
||||
/>
|
||||
<MenuButton v-model="toggledOn" class="ToggleIcon">
|
||||
<SvgIcon :name="icon" />
|
||||
<div v-if="label" v-text="label" />
|
||||
</MenuButton>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ToggleIcon {
|
||||
margin: -4px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toggledOff {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user