Skip to content

Commit

Permalink
feat: input asset amount for burning (#5292)
Browse files Browse the repository at this point in the history
* chore: extract burn popup into own component

* feat: add asset amount slider input

* fix: fix amount boundries

* feat: burn selected amount
  • Loading branch information
MarkNerdi996 committed Nov 29, 2022
1 parent 6d573c3 commit 2986c71
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 32 deletions.
119 changes: 119 additions & 0 deletions packages/shared/components/inputs/AssetAmountSliderInput.svelte
@@ -0,0 +1,119 @@
<script lang="typescript">
import { Text, FontWeight, AssetIcon, InputContainer, AmountInput, SliderInput, UnitInput } from 'shared/components'
import { convertToRawAmount, formatTokenAmountBestMatch, formatTokenAmountDefault, IAsset } from '@core/wallet'
import { IOTA_UNIT_MAP } from '@core/utils'
import { parseCurrency } from '@core/i18n/utils/parseCurrency'
import { localize } from '@core/i18n'
import Big from 'big.js'
export let inputElement: HTMLInputElement = undefined
export let disabled = false
export let isFocused = false
export let asset: IAsset
export let rawAmount: string = undefined
export let unit: string = undefined
let amountInputElement: HTMLInputElement
let error: string
let amount: string = rawAmount
? formatTokenAmountDefault(Number(rawAmount), asset?.metadata, unit, false)
: undefined
$: isFocused && (error = '')
let allowedDecimals = 0
$: if (!asset?.metadata?.useMetricPrefix) {
if (unit === asset?.metadata.unit) {
allowedDecimals = Math.min(asset?.metadata.decimals, 18)
} else if (unit === asset?.metadata?.subunit) {
allowedDecimals = 0
}
} else if (asset?.metadata?.useMetricPrefix) {
allowedDecimals = IOTA_UNIT_MAP?.[unit?.substring(0, 1)] ?? 0
}
$: bigAmount = convertToRawAmount(amount, unit, asset?.metadata)
export function validate(): Promise<void> {
const amountAsFloat = parseCurrency(amount)
const isAmountZeroOrNull = !Number(amountAsFloat)
// Zero value transactions can still contain metadata/tags
error = ''
if (isAmountZeroOrNull) {
error = localize('error.send.amountInvalidFormat')
} else if (
(unit === asset?.metadata?.subunit ||
(unit === asset?.metadata?.unit && asset?.metadata?.decimals === 0)) &&
Number.parseInt(amount, 10).toString() !== amount
) {
error = localize('error.send.amountNoFloat')
} else if (bigAmount.gt(Big(asset?.balance?.available))) {
error = localize('error.send.amountTooHigh')
} else if (bigAmount.lte(Big(0))) {
error = localize('error.send.amountZero')
} else if (!bigAmount.mod(1).eq(Big(0))) {
error = localize('error.send.amountSmallerThanSubunit')
}
if (error) {
return Promise.reject(error)
}
rawAmount = bigAmount.toString()
}
</script>

<InputContainer
bind:this={inputElement}
bind:inputElement={amountInputElement}
col
{isFocused}
{error}
classes="space-y-5"
on:clickOutside={() => (isFocused = false)}
>
<div class="flex flex-row w-full items-center space-x-0.5 relative">
<div
class="flex flex-row items-center p-2 space-x-2 text-left bg-gray-100 dark:bg-gray-700 rounded-md cursor-default"
>
<AssetIcon small {asset} />
<div class="w-full relative" style="max-width: 75px;">
<Text
color="gray-600"
darkColor="white"
fontWeight={FontWeight.semibold}
fontSize="15"
classes="overflow-hidden whitespace-nowrap overflow-ellipsis"
>
{asset?.metadata?.name ?? asset?.id}
</Text>
</div>
</div>
<AmountInput
bind:inputElement={amountInputElement}
bind:amount
bind:hasFocus={isFocused}
maxDecimals={allowedDecimals}
isInteger={allowedDecimals === 0}
clearBackground
clearPadding
clearBorder
{disabled}
/>
<UnitInput bind:unit bind:isFocused tokenMetadata={asset?.metadata} />
</div>
<div class="flex flex-col">
<SliderInput
bind:value={amount}
max={Number(formatTokenAmountDefault(asset?.balance?.available, asset.metadata, unit, false))}
decimals={asset.metadata.decimals}
/>
<div class="flex flex-row justify-between">
<Text color="gray-800" darkColor="gray-500" fontSize="xs"
>{formatTokenAmountBestMatch(0, asset?.metadata)}</Text
>
<Text color="gray-800" darkColor="gray-500" fontSize="xs"
>{formatTokenAmountBestMatch(asset?.balance?.available, asset?.metadata)}</Text
>
</div>
</div>
</InputContainer>
227 changes: 227 additions & 0 deletions packages/shared/components/inputs/SliderInput.svelte
@@ -0,0 +1,227 @@
<script>
import { createEventDispatcher } from 'svelte'
// Props
export let min = 0
export let max = 100
export let initialValue = 0
export let id = null
export let decimals = 0
export let value = typeof initialValue === 'string' ? parseInt(initialValue) : initialValue
// Node Bindings
let container = null
let thumb = null
let progressBar = null
let element = null
// Internal State
let elementX = null
let currentThumb = null
let holding = false
let thumbHover = false
// Dispatch 'change' events
const dispatch = createEventDispatcher()
// Mouse shield used onMouseDown to prevent any mouse events penetrating other elements,
// ie. hover events on other elements while dragging. Especially for Safari
const mouseEventShield = document.createElement('div')
mouseEventShield.setAttribute('class', 'mouse-over-shield')
mouseEventShield.addEventListener('mouseover', (e) => {
e.preventDefault()
e.stopPropagation()
})
function resizeWindow() {
elementX = element.getBoundingClientRect().left
}
// Allows both bind:value and on:change for parent value retrieval
function setValue(val) {
value = String(val)
dispatch('change', { value })
}
function onTrackEvent(e) {
// Update value immediately before beginning drag
updateValueOnEvent(e)
onDragStart(e)
}
function onDragStart(e) {
// If mouse event add a pointer events shield
if (e.type === 'mousedown') document.body.append(mouseEventShield)
currentThumb = thumb
}
function onDragEnd(e) {
// If using mouse - remove pointer event shield
if (e.type === 'mouseup') {
if (document.body.contains(mouseEventShield)) document.body.removeChild(mouseEventShield)
// Needed to check whether thumb and mouse overlap after shield removed
if (isMouseInElement(e, thumb)) thumbHover = true
}
currentThumb = null
}
// Check if mouse event cords overlay with an element's area
function isMouseInElement(event, element) {
const rect = element.getBoundingClientRect()
const { clientX: x, clientY: y } = event
if (x < rect.left || x >= rect.right) return false
if (y < rect.top || y >= rect.bottom) return false
return true
}
function calculateNewValue(clientX) {
// Find distance between cursor and element's left cord (20px / 2 = 10px) - Center of thumb
const delta = clientX - (elementX + 10)
// Use width of the container minus (5px * 2 sides) offset for percent calc
let percent = (delta * 100) / (container.clientWidth - 10)
// Limit percent 0 -> 100
percent = percent < 0 ? 0 : percent > 100 ? 100 : percent
// Limit value min -> max
setValue(parseInt((percent / 100) * (max - min) * 10 ** decimals) / 10 ** decimals + min)
}
// Handles both dragging of touch/mouse as well as simple one-off click/touches
function updateValueOnEvent(e) {
// touchstart && mousedown are one-off updates, otherwise expect a currentPointer node
if (!currentThumb && e.type !== 'touchstart' && e.type !== 'mousedown') return false
if (e.stopPropagation) e.stopPropagation()
if (e.preventDefault) e.preventDefault()
// Get client's x cord either touch or mouse
const clientX = e.type === 'touchmove' || e.type === 'touchstart' ? e.touches[0].clientX : e.clientX
calculateNewValue(clientX)
}
// React to left position of element relative to window
$: if (element) elementX = element.getBoundingClientRect().left
// Set a class based on if dragging
$: holding = Boolean(currentThumb)
// Update progressbar and thumb styles to represent value
$: if (progressBar && thumb) {
let percent = ((Number(value) - min) * 100) / (max - min)
percent = Math.max(Math.min(percent, 100), 0)
const offsetLeft = (container.clientWidth - 10) * (percent / 100) + 5
// Update thumb position + active range track width
thumb.style.left = `${offsetLeft}px`
progressBar.style.width = `${offsetLeft}px`
}
</script>

<svelte:window
on:touchmove|nonpassive={updateValueOnEvent}
on:touchcancel={onDragEnd}
on:touchend={onDragEnd}
on:mousemove={updateValueOnEvent}
on:mouseup={onDragEnd}
on:resize={resizeWindow}
/>
<div class="range">
<div
class="range__wrapper"
tabindex="0"
bind:this={element}
role="slider"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
{id}
on:mousedown={onTrackEvent}
on:touchstart={onTrackEvent}
>
<div class="range__track" bind:this={container}>
<div class="range__track--highlighted bg-blue-500" bind:this={progressBar} />
<div
class="range__thumb bg-blue-500"
class:range__thumb--holding={holding}
bind:this={thumb}
on:touchstart={onDragStart}
on:mousedown={onDragStart}
on:mouseover={() => (thumbHover = true)}
on:focus={() => (thumbHover = true)}
on:mouseout={() => (thumbHover = false)}
on:blur={() => (thumbHover = false)}
/>
</div>
</div>
</div>

<svelte:head>
<style>
.mouse-over-shield {
position: fixed;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
background-color: rgba(255, 0, 0, 0);
z-index: 10000;
cursor: grabbing;
}
</style>
</svelte:head>

<style>
.range {
position: relative;
flex: 1;
cursor: pointer;
}
.range__wrapper {
min-width: 100%;
position: relative;
padding: 0.5rem;
box-sizing: border-box;
outline: none;
}
.range__wrapper:focus-visible > .range__track {
box-shadow: 0 0 0 2px white, 0 0 0 3px var(--track-focus, #6185ff);
}
.range__track {
height: 6px;
background-color: var(--track-bgcolor, #d8e3f5);
border-radius: 999px;
}
.range__track--highlighted {
width: 0;
height: 6px;
position: absolute;
border-radius: 999px;
}
.range__thumb {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 20px;
height: 20px;
cursor: pointer;
border-radius: 999px;
margin-top: -8px;
transition: box-shadow 100ms;
user-select: none;
box-shadow: var(--thumb-boxshadow, 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 0px 2px 1px rgba(0, 0, 0, 0.2));
}
.range__thumb--holding {
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 2px 1px rgba(0, 0, 0, 0.2),
0 0 0 6px var(--thumb-holding-outline, rgba(113, 119, 250, 0.3));
}
</style>
2 changes: 2 additions & 0 deletions packages/shared/components/inputs/index.js
Expand Up @@ -2,6 +2,7 @@ export { default as AccountInput } from './AccountInput.svelte'
export { default as AliasInput } from './AliasInput.svelte'
export { default as AmountInput } from './AmountInput.svelte'
export { default as AssetAmountInput } from './AssetAmountInput.svelte'
export { default as AssetAmountSliderInput } from './AssetAmountSliderInput.svelte'
export { default as AssetDropdown } from './AssetDropdown.svelte'
export { default as Checkbox } from './Checkbox.svelte'
export { default as ClosableInput } from './ClosableInput.svelte'
Expand All @@ -21,6 +22,7 @@ export { default as PasswordInput } from './PasswordInput.svelte'
export { default as PinInput } from './PinInput.svelte'
export { default as Radio } from './Radio.svelte'
export { default as RecipientInput } from './RecipientInput.svelte'
export { default as SliderInput } from './SliderInput.svelte'
export { default as TextInput } from './TextInput.svelte'
export { default as TogglableButton } from './TogglableButton.svelte'
export { default as Toggle } from './Toggle.svelte'
Expand Down

0 comments on commit 2986c71

Please sign in to comment.