Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: input asset amount for burning (#5292)
* chore: extract burn popup into own component * feat: add asset amount slider input * fix: fix amount boundries * feat: burn selected amount
- Loading branch information
1 parent
6d573c3
commit 2986c71
Showing
9 changed files
with
407 additions
and
32 deletions.
There are no files selected for viewing
119 changes: 119 additions & 0 deletions
119
packages/shared/components/inputs/AssetAmountSliderInput.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.