包含draggable
或moveable
手柄的元素只能从top
left
位置开始,我不知道如何调整,计算或校正像素值使可拖动的手柄元素像普通图形一样从左下角开始。
handle
的位置用于生成范围限制值,例如0-100
和x
轴的y
之间的百分比值,而与元素的像素无关尺寸。
这是一种打算在颜色选择器小部件中使用的范围输入或位置选择器。
颜色渐变的变化取决于窗口小部件在某物的左侧,顶部或右侧的相对位置,因此选择器或手柄应相应地调整其范围的起点。
我正在使用onpointermove
获取x
和y
的{{1}}和div.handle
位置
调整父元素的相对偏移width
,height
,left
和top
。
我一生无法弄清楚的是允许范围输入从任意角度(最好是bottom
left
)跟踪位置所需的数学和代码。
很抱歉使用自定义库,但此示例主要是普通示例,至少是重要的计算。
const {dom, component, each, on, once, isNum, $, run} = rilti
// keep a number between a minimum and maximum ammount
const clamp = (n, min, max) => Math.min(Math.max(n, min), max)
// define element behavior
component('range-input', {
// set up everything before element touches DOM
create (range /* Proxy<Function => Element> */) {
// setup zero values in state (observer-like abstraction tracking changes)
range.state({value: 0, valueX: 0, valueY: 0})
// local vars for easier logic
let Value, ValueY
// create element <div class="handle"> and append to <range-input>
// also add property to range and get it as a const
const handle = range.handle = dom.div.handle({$: range})
// set the range limits at 0-100% by default for X and Y axis
if (range.limit == null) range.limitX = range.limit = 100
if (range.limit !== range.limitX) range.limitX = range.limit
if (range.limitY == null) range.limitY = range.limit
// set the X position by percentage/range number,
// move the handle accordingly and change state
range.setX = (value = range.value || 0, skipChecks) => {
if (!skipChecks && value === Value) return
if (value > range.limitX || value < 0) throw new Error('value out of range')
// if the element is not in the dom
// then wait for it to mount first
if (!range.mounted) {
range.once.mount(e => range.setX(value))
return
}
// allow float values or round it to ints by default
if (!range.decimals) value = Math.round(value)
const hWidth = handle.offsetWidth
// get pixel range
const Min = hWidth / 2
const Max = range.offsetWidth - Min
// calculate pixel postion from range value
const hLeft = (value / range.limitX) * (Max - Min)
handle.style.left = hLeft + 'px'
// update all the states
Value = range.state.value = range.state.valueX = value
}
// same as setX but for Y axis
range.setY = (value = range.valueY || 0, skipChecks) => {
if (!skipChecks && value === Value) return
if (value > range.limitY || value < 0) throw new Error('value out of range')
if (!range.mounted) {
range.once.mount(e => range.setY(value))
return
}
const hHeight = handle.offsetHeight
const Min = hHeight / 2
const Max = range.offsetHeight - Min
const hTop = (value / range.limitY) * (Max - Min)
handle.style.top = hTop + 'px'
if (!range.decimals) value = Math.round(value)
ValueY = range.state.valueY = value
}
// get the raw Element/Node and define (s/g)etters
Object.defineProperties(range() /* -> <range-input> */, {
value: {get: () => Value, set: range.setX},
valueX: {get: () => Value, set: range.setX},
valueY: {get: () => ValueY, set: range.setY}
})
let rWidth // range.offsetWidth
let rHeight // range.offsetHeight
let rRect // cache of range.getBoundingClientRect()
// called when user moves the handle
const move = (x = 0, y = 0) => {
// check the the axis is not locked
// for when you want to use range-input as a slider
if (!range.lockX) {
// adjust for relative position
if (x < rRect.left) x = rRect.left
else if (x > rRect.left + rWidth) x = rRect.left + rWidth
x -= rRect.left
const hWidth = handle.offsetWidth
// get pixel range
const min = hWidth / 2
const max = rWidth - min
// keep it inside the block
const hLeft = clamp(x, min, max) - min
handle.style.left = hLeft + 'px'
// pixel position -> percentage/value
let value = (hLeft * range.limitX) / (max - min)
// round value to an int by default
if (!range.decimals) value = Math.round(value)
// set it if it's not the same as the old value
if (value !== Value) {
Value = range.state.value = range.state.valueX = value
}
}
// now do below as above for Y axis
if (!range.lockY) { // when it's not locked
if (y < rRect.top) y = rRect.top
else if (y > rRect.top + rWidth) y = rRect.top + rHeight
y -= rRect.top
const hHeight = handle.offsetHeight
const min = hHeight / 2
const max = range.offsetHeight - min
const hTop = clamp(y, min, max) - min
handle.style.top = hTop + 'px'
let value = (hTop * range.limitY) / (max - min)
if (!range.decimals) value = Math.round(value)
if (value !== ValueY) {
ValueY = range.state.valueY = value
}
}
// .dispatchEvent(new CustomEvent('input'))
range.emit('input')
// call an update function if it's present as a prop
if (range.update) range.update(range, handle)
}
// track and manage starting, stopping and moving events
// for .pointer(up/down/move) event types respectively.
const events = range.state.events = {
move: on.pointermove(document, e => move(e.x, e.y)).off(),
stop: on.pointerup(document, () => {
events.move.off()
events.start.on()
}).off(),
start: once.pointerdown([range, handle], () => {
[rWidth, rHeight] = [range.offsetWidth, range.offsetHeight]
rRect = range.getBoundingClientRect()
events.move.on()
events.stop.on()
}).off()
}
// ^-- all the events are off at the start
// they get turned on when the element mounts
},
// when Element enters DOM set the positions
mount (range) {
if (!range.lockY) range.handle.style.top = 0
range.setX()
range.setY()
// start listening for user interactions
range.state.events.start.on()
},
// start listening again on DOM re-entry
remount (range) {
range.state.events.start.on()
},
// stop listening when removed from DOM
unmount ({state: {events}}) { each(events, e => e.off()) },
// track custom attribute to set some props conveniently
attr: {
opts (range, val) {
run(() => // wait for DOMContentLoaded first
val.split(';')
.filter(v => v != null && v.length)
.map(pair => pair.trim().split(':').map(part => part.trim()))
.forEach(([prop, value]) => {
if (value.toLowerCase() === 'true') value = true
else if (value.toLowerCase() === 'false') value = false
else {
const temp = Number(value)
if (isNum(temp)) value = temp
}
if (prop === 'x' || prop === 'v') {
range.setX(value, true)
} else if (prop === 'y') {
range.setY(value, true)
} else {
range[prop] = value
}
})
)
}
}
})
// show the values of the range-input
$('span.stats').append($('range-input').state`
X: ${'valueX'}%, Y: ${'valueY'}%
`)
// add a title
dom.h4('<range-input>: custom element').prependTo('body')
range-input {
position: relative;
display: block;
margin: 1em auto;
width: 250px;
height: 250px;
border: 1px solid #ccc;
}
range-input > div.handle {
position: absolute;
background: #ccc;
width: 20px;
height: 20px;
cursor: grab;
user-drag: none;
user-select: none;
touch-action: none;
}
.details {
width: 225px;
text-align: left;
margin: 3em auto;
}
* {
box-sizing: border-box;
}
body {
text-align: center;
color: hsl(0,0%,40%);
}
h4 {
margin: 0 auto;
}
<range-input opts="x: 35; y: 80;"></range-input>
<span class="stats"></span>
<section class="details">
<p>
<b>Please Help:</b><br>
I can't figure out how to code it so that
the range-input could start at an arbitrary corner
instead of just top left.
I'd like it to start counting from bottom left instead.
</p>
<pre style="text-align: left;"><code>
// the handle should be able to start at
left: 0;
bottom: 0;
// with X/Y being zero;
// not sure how to achieve this.
</code></pre>
</section>
<script src="https://rawgit.com/SaulDoesCode/rilti.js/experimental/dist/rilti.js"></script>
相同的例子 Codepen
答案 0 :(得分:1)
描述问题的另一种方法是,当y轴应该从0变为100时,y轴将从100(技术上为limitY)变为0。因此,我们可以通过完全计算来稍微更改代码以反转该轴y百分比,然后减去100。(即100-80 = 20或100-35-65。)这会将高值更改为低值,反之亦然。然后,如果我们要从百分比转换为像素,只需将其再次从100减去即可获得原始的翻转百分比(您已经完成了所有工作。)
更改的两行是:
const hTop = (value / range.limitY) * (Max - Min)
成为
const hTop = (1 - value / range.limitY) * (Max - Min)
// 1 - value / range.limitY is a shortening of (range.limitY - value) / range.limitY
和
let value = (hTop * range.limitY) / (max - min)
成为
let value = range.limitY * (1 - hTop / (max - min))
// this is also a shortening, you could have written it,
// value = range.limitY - (hTop * range.limitY) / (max - min)
这里是Codepen。
同样,如果要翻转x轴,则可以在代码的那部分使用类似的逻辑。您可以翻转两个轴的各种组合以从各个角开始。
同一个问题的一个较难的版本(练习的一个很好的练习)是如何正确地将像素从像素转换成百分比,以及从a
到b
的任何范围b
可能比a
小。