Skip to content

Commit

Permalink
fix: #110 ContextMenu closes when hovering a validation error (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong committed Jul 6, 2022
1 parent 89d661a commit 46424bb
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
let refNavigationBarItem: Element | undefined
let open = false
let popupId: number | undefined
$: itemPath = path.slice(0, index)
$: selectedItem = path[index]
function handleSelectItem(item) {
closeAbsolutePopup()
closeAbsolutePopup(popupId)
onSelect(itemPath.concat(item))
}
Expand All @@ -35,7 +36,7 @@
onSelect: handleSelectItem
}
openAbsolutePopup(NavigationBarDropdown, props, {
popupId = openAbsolutePopup(NavigationBarDropdown, props, {
anchor: refNavigationBarItem,
closeOnOuterClick: true,
onClose: () => {
Expand Down
21 changes: 12 additions & 9 deletions src/lib/components/controls/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import Tooltip from './Tooltip.svelte'

export function tooltip(node: Element, { text, openAbsolutePopup, closeAbsolutePopup }) {
function handleMouseOver() {
let popupId: number | undefined

function handleMouseEnter() {
const props = {
text //: validationError.isChildError ? 'Contains invalid data' : validationError.message
text
}

openAbsolutePopup(Tooltip, props, {
// opening popup will fail if there is already a popup open
popupId = openAbsolutePopup(Tooltip, props, {
position: 'top',
width: 10 * text.length, // rough estimate of the width of the message
offsetTop: 3,
Expand All @@ -15,17 +18,17 @@ export function tooltip(node: Element, { text, openAbsolutePopup, closeAbsoluteP
})
}

function handleMouseOut() {
closeAbsolutePopup()
function handleMouseLeave() {
closeAbsolutePopup(popupId)
}

node.addEventListener('mouseover', handleMouseOver)
node.addEventListener('mouseout', handleMouseOut)
node.addEventListener('mouseenter', handleMouseEnter)
node.addEventListener('mouseleave', handleMouseLeave)

return {
destroy() {
node.removeEventListener('mouseover', handleMouseOver)
node.removeEventListener('mouseout', handleMouseOut)
node.removeEventListener('mouseenter', handleMouseEnter)
node.removeEventListener('mouseleave', handleMouseLeave)
}
}
}
5 changes: 3 additions & 2 deletions src/lib/components/modals/TransformModalHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
export let onChangeQueryLanguage: OnChangeQueryLanguage
let refConfigButton: HTMLButtonElement | undefined
let popupId: number | undefined
const { close } = getContext('simple-modal')
const { openAbsolutePopup, closeAbsolutePopup } = getContext('absolute-popup')
Expand All @@ -21,12 +22,12 @@
queryLanguages,
queryLanguageId,
onChangeQueryLanguage: (selectedQueryLanguage) => {
closeAbsolutePopup()
closeAbsolutePopup(popupId)
onChangeQueryLanguage(selectedQueryLanguage)
}
}
openAbsolutePopup(SelectQueryLanguage, props, {
popupId = openAbsolutePopup(SelectQueryLanguage, props, {
position: 'bottom',
offsetTop: -2,
offsetLeft: 0,
Expand Down
158 changes: 26 additions & 132 deletions src/lib/components/modals/popup/AbsolutePopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,156 +2,50 @@

<script lang="ts">
import { createDebug } from '../../../utils/debug'
import { setContext, tick } from 'svelte'
import { isChildOf } from '$lib/utils/domUtils'
import { keyComboFromEvent } from '../../../utils/keyBindings'
import type { AbsolutePopupOptions } from '../../../types'
import { setContext } from 'svelte'
import type { PopupEntry } from '../../../types'
import { uniqueId } from '../../../utils/uniqueId'
import AbsolutePopupEntry from './AbsolutePopupEntry.svelte'
const debug = createDebug('jsoneditor:AbsolutePopup')
let popupComponent = null
let popupProps = null
let popupOptions: AbsolutePopupOptions = {}
let popups: PopupEntry[] = []
let refRootPopup
let refHiddenInput
function openAbsolutePopup(component, props, options): number {
debug('open...', props, options)
function openAbsolutePopup(Component, props, options) {
debug('open...', options)
popupComponent = Component
popupProps = props || {}
popupOptions = options || {}
tick().then(focus)
}
function closeAbsolutePopup() {
if (popupComponent) {
const onClose = popupOptions.onClose
popupComponent = null
popupProps = null
popupOptions = {}
if (onClose) {
onClose()
}
}
}
function closeWhenOutside(event) {
if (
popupOptions &&
popupOptions.closeOnOuterClick &&
!isChildOf(event.target, (e) => e === refRootPopup)
) {
closeAbsolutePopup()
const popup = {
id: uniqueId(),
component: component,
props: props || {},
options: options || {}
}
}
function handleWindowMouseDown(event) {
closeWhenOutside(event)
}
function handleMouseDownInside(event) {
event.stopPropagation()
}
function handleKeyDown(event) {
const combo = keyComboFromEvent(event)
if (combo === 'Escape') {
closeAbsolutePopup()
}
}
popups = [...popups, popup]
function handleScrollWheel(event) {
closeWhenOutside(event)
return popup.id
}
function calculateStyle() {
function calculatePosition() {
if (popupOptions.anchor) {
const {
anchor,
width = 0,
height = 0,
offsetTop = 0,
offsetLeft = 0,
position
} = popupOptions
const { left, top, bottom, right } = anchor.getBoundingClientRect()
const positionAbove =
position === 'top' || (top + height > window.innerHeight && top > height)
const positionLeft =
position === 'left' || (left + width > window.innerWidth && left > width)
return {
left: positionLeft ? right - offsetLeft : left + offsetLeft,
top: positionAbove ? top - offsetTop : bottom + offsetTop,
positionAbove,
positionLeft
}
} else if (typeof popupOptions.left === 'number' && typeof popupOptions.top === 'number') {
const { left, top, width = 0, height = 0 } = popupOptions
function closeAbsolutePopup(popupId: number | undefined) {
const popupIndex = popups.findIndex((popup) => popup.id === popupId)
const positionAbove = top + height > window.innerHeight && top > height
const positionLeft = left + width > window.innerWidth && left > width
return {
left,
top,
positionAbove,
positionLeft
}
} else {
throw new Error('Invalid config: pass either "left" and "top", or pass "anchor"')
if (popupIndex !== -1) {
const popup = popups[popupIndex]
if (popup.options.onClose) {
popup.options.onClose()
}
}
const rootRect = refRootPopup.getBoundingClientRect()
const { left, top, positionAbove, positionLeft } = calculatePosition()
const verticalStyling = positionAbove
? `bottom: ${rootRect.top - top}px;`
: `top: ${top - rootRect.top}px;`
const horizontalStyling = positionLeft
? `right: ${rootRect.left - left}px;`
: `left: ${left - rootRect.left}px;`
return verticalStyling + horizontalStyling
}
function focus() {
if (refHiddenInput) {
refHiddenInput.focus()
popups = popups.filter((popup) => popup.id !== popupId)
}
}
$: debug('popups', popups)
setContext('absolute-popup', { openAbsolutePopup, closeAbsolutePopup })
</script>

<svelte:window
on:mousedown|capture={handleWindowMouseDown}
on:keydown|capture={handleKeyDown}
on:wheel|capture={handleScrollWheel}
/>

<div
bind:this={refRootPopup}
class="jse-absolute-popup"
on:mousedown={handleMouseDownInside}
on:keydown={handleKeyDown}
>
{#if popupComponent && popupProps}
<div class="jse-absolute-popup-content" style={calculateStyle()}>
<input bind:this={refHiddenInput} class="jse-hidden-input" />
<svelte:component this={popupComponent} {...popupProps} />
</div>
{/if}
</div>
{#each popups as popup}
<AbsolutePopupEntry {popup} {closeAbsolutePopup} />
{/each}

<slot />

<style src="./AbsolutePopup.scss"></style>
119 changes: 119 additions & 0 deletions src/lib/components/modals/popup/AbsolutePopupEntry.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<script lang="ts">
import type { AbsolutePopupOptions, PopupEntry } from '../../../types'
import { onMount } from 'svelte'
import { isChildOf } from '../../../utils/domUtils'
import { keyComboFromEvent } from '../../../utils/keyBindings'
export let popup: PopupEntry
export let closeAbsolutePopup: (popupId: number) => void
let refRootPopup
let refHiddenInput
onMount(focus)
function closeWhenOutside(event) {
if (
popup.options &&
popup.options.closeOnOuterClick &&
!isChildOf(event.target, (e) => e === refRootPopup)
) {
closeAbsolutePopup(popup.id)
}
}
function handleWindowMouseDown(event) {
closeWhenOutside(event)
}
function handleMouseDownInside(event) {
event.stopPropagation()
}
function handleKeyDown(event) {
const combo = keyComboFromEvent(event)
if (combo === 'Escape') {
closeAbsolutePopup(popup.id)
}
}
function handleScrollWheel(event) {
closeWhenOutside(event)
}
function calculateStyle(refRootPopup, options: AbsolutePopupOptions) {
function calculatePosition() {
if (options.anchor) {
const { anchor, width = 0, height = 0, offsetTop = 0, offsetLeft = 0, position } = options
const { left, top, bottom, right } = anchor.getBoundingClientRect()
const positionAbove =
position === 'top' || (top + height > window.innerHeight && top > height)
const positionLeft =
position === 'left' || (left + width > window.innerWidth && left > width)
return {
left: positionLeft ? right - offsetLeft : left + offsetLeft,
top: positionAbove ? top - offsetTop : bottom + offsetTop,
positionAbove,
positionLeft
}
} else if (typeof options.left === 'number' && typeof options.top === 'number') {
const { left, top, width = 0, height = 0 } = options
const positionAbove = top + height > window.innerHeight && top > height
const positionLeft = left + width > window.innerWidth && left > width
return {
left,
top,
positionAbove,
positionLeft
}
} else {
throw new Error('Invalid config: pass either "left" and "top", or pass "anchor"')
}
}
const rootRect = refRootPopup.getBoundingClientRect()
const { left, top, positionAbove, positionLeft } = calculatePosition()
const verticalStyling = positionAbove
? `bottom: ${rootRect.top - top}px;`
: `top: ${top - rootRect.top}px;`
const horizontalStyling = positionLeft
? `right: ${rootRect.left - left}px;`
: `left: ${left - rootRect.left}px;`
return verticalStyling + horizontalStyling
}
function focus() {
if (refHiddenInput) {
refHiddenInput.focus()
}
}
</script>

<svelte:window
on:mousedown|capture={handleWindowMouseDown}
on:keydown|capture={handleKeyDown}
on:wheel|capture={handleScrollWheel}
/>

<div
bind:this={refRootPopup}
class="jse-absolute-popup"
on:mousedown={handleMouseDownInside}
on:keydown={handleKeyDown}
>
{#if refRootPopup}
<div class="jse-absolute-popup-content" style={calculateStyle(refRootPopup, popup.options)}>
<input bind:this={refHiddenInput} class="jse-hidden-input" />
<svelte:component this={popup.component} {...popup.props} />
</div>
{/if}
</div>

<style src="./AbsolutePopupEntry.scss"></style>
Loading

0 comments on commit 46424bb

Please sign in to comment.