Simple Module For Creating Draggable GUI Elements

Thank you for making this module, I’ve been using it in my game and it works well.
Except I just noticed today something disturbing with it while I was working on a custom inventory system: If you are playing a touchscreen enabled device and start touching somewhere in your screen, then hovering to the gui object, it will start dragging it, although I started touching outside of the object. It doesn’t happen with a mouse though. Here is maybe a video so you understand better:


Will you fix that or should I maybe just try to fix it myself ?

2 Likes

Is there a way to make a separate Frame move along with another frame?

Example:
Red GUI should move with yellow GUI.

image

I see. Yea sorry, I don’t have time to fix it right now. I suggest giving it a shot at fixing it yourself

You can do that by saving the offset of the yellow to the red gui, and then whenever the red gui moves you update the yellow gui to be the red gui’s new position plus its initial offset.

Example

-- Calculate yellow and red gui's offsets from each other
local yellowOffset = yellowGui.Position - redGui.Position
local redOffset = redGui.Position - yellowGui.Position

-- Create draggable objects
local yellowDrag = DraggableObject.new(yellowGui)
local redDrag = DraggableObject.new(redGui)

-- Whenever the yellow gui is dragged, update the red gui's position
yellowDrag.Dragged = function(yellowPos)
   redGui.Position = yellowPos + redOffset
end

-- Whenever the red gui is dragged, update the yellow gui's position
redDrag.Dragged = function(redPos)
   yellowGui.Position = redPos + yellowOffset
end

yellowDrag:Enable() -- Enable dragging of yellow gui
redDrag:Enable() -- Enable dragging of red gui
3 Likes

Thanks! :grinning_face_with_smiling_eyes:

1 Like

Alright I fixed that problem by listening the InputBegan event of UserInputService instead of the object’s event and making sure the touch started inside the object:
DraggableObject.lua (3.3 KB)

3 Likes

nice bro this is an amazing resource.

3 Likes

Is there going to be new updates to this?

If so, features where you can enable dragging a GuiObject on only one axis would be great.

1 Like

I got two errors:
Players.GEILER123456.PlayerScripts.Client.DragModule:40: invalid argument #2 (Vector3 expected, got nil)
and
Players.GEILER123456.PlayerScripts.Client.DragModule:42: attempt to index nil with ‘X’

Maybe my code is wrong, here it is:

        local pressing = false
        local pressedFor = 0
        local button1Down
        local button1Up
        local cloneDrag = DragModule.new(clone)

        button1Down = clone.MouseButton1Down:Connect(function()
            pressing = true
            while pressing do
                pressedFor += RunService.Heartbeat:Wait()

                if pressedFor > 1 then
                    cloneDrag:Enable()
                    break
                end
            end
        end)

        button1Up = clone.MouseButton1Up:Connect(function()
            pressing = false

            if pressedFor < 1 then
                print("Client fire drop!")
                freeSlot.Item = nil
                dropRemote:FireServer(name)
                clone:Destroy()
                button1Down:Disconnect()
                button1Up:Disconnect()
            else
                print("It was just for positioning")
                cloneDrag:Disable()
            end
            pressedFor = 0
        end)

I attempted to fix it myself:
Changed local startPos = nil to local startPos = object.Position,
Changed local dragStart = nil to local dragStart = Vector3.new(0, 0, 0)

After these changes, it doesn’t throw the errors anymore.

Here the changed version:

--[[
	@Author: Spynaz
	@Description: Enables dragging on GuiObjects. Supports both mouse and touch.
	
	For instructions on how to use this module, go to this link:
	https://devforum.roblox.com/t/simple-module-for-creating-draggable-gui-elements/230678
--]]

local UDim2_new = UDim2.new

local UserInputService = game:GetService("UserInputService")

local DraggableObject 		= {}
DraggableObject.__index 	= DraggableObject

-- Sets up a new draggable object
function DraggableObject.new(Object)
	local self 			= {}
	self.Object			= Object
	self.DragStarted	= nil
	self.DragEnded		= nil
	self.Dragged		= nil
	self.Dragging		= false

	setmetatable(self, DraggableObject)
	
	return self
end

-- Enables dragging
function DraggableObject:Enable()
	local object			= self.Object
	local dragInput			= nil
	local dragStart			= Vector3.new(0, 0, 0)
	local startPos			= object.Position
	local preparingToDrag	= false
	
	-- Updates the element
	local function update(input)
		local delta 		= input.Position - dragStart
		local newPosition	= UDim2_new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y)
		object.Position 	= newPosition
	
		return newPosition
	end
	
	self.InputBegan = object.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			preparingToDrag = true
			--[[if self.DragStarted then
				self.DragStarted()
			end
			
			dragging	 	= true
			dragStart 		= input.Position
			startPos 		= Element.Position
			--]]
			
			local connection 
			connection = input.Changed:Connect(function()
				if input.UserInputState == Enum.UserInputState.End and (self.Dragging or preparingToDrag) then
					self.Dragging = false
					connection:Disconnect()
					
					if self.DragEnded and not preparingToDrag then
						self.DragEnded()
					end
					
					preparingToDrag = false
				end
			end)
		end
	end)
	
	self.InputChanged = object.InputChanged:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
			dragInput = input
		end
	end)
	
	self.InputChanged2 = UserInputService.InputChanged:Connect(function(input)
		if object.Parent == nil then
			self:Disable()
			return
		end
		
		if preparingToDrag then
			preparingToDrag = false
			
			if self.DragStarted then
				self.DragStarted()
			end
			
			self.Dragging	= true
			dragStart 		= input.Position
			startPos 		= object.Position
		end
		
		if input == dragInput and self.Dragging then
			local newPosition = update(input)
			
			if self.Dragged then
				self.Dragged(newPosition)
			end
		end
	end)
end

-- Disables dragging
function DraggableObject:Disable()
	self.InputBegan:Disconnect()
	self.InputChanged:Disconnect()
	self.InputChanged2:Disconnect()
	
	if self.Dragging then
		self.Dragging = false
		
		if self.DragEnded then
			self.DragEnded()
		end
	end
end

return DraggableObject

Good job on this module!

2 Likes

I was getting a very annoying bug where my draggable GUI would go off screen whenever I happen to be moving diagonally. It turns out dragStart was being reset to the origin since clicking and dragging and pressing input keys with no position at the right moment can break the dragging. For anyone curious on how to fix this, similar to how self.InputChanged checks input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch, you must do this same check in self.InputChanged2 along with preparingToDrag to prevent non-dragging related input positions to affect dragStart

TL;DR If GUI dragging is glitchy (disappears off screen / teleports somewhere else) when moving, use this instead

	@Author: Spynaz
	@Description: Enables dragging on GuiObjects. Supports both mouse and touch.
	
	For instructions on how to use this module, go to this link:
	https://devforum.roblox.com/t/simple-module-for-creating-draggable-gui-elements/230678
--]]

local UDim2_new = UDim2.new

local UserInputService = game:GetService("UserInputService")

local DraggableObject 		= {}
DraggableObject.__index 	= DraggableObject

-- Check if either mouse movement or touch input
function MouseOrTouchMovement(input)
	return input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch
end

-- Sets up a new draggable object
function DraggableObject.new(Object)
	local self 			= {}
	self.Object			= Object
	self.DragStarted	= nil
	self.DragEnded		= nil
	self.Dragged		= nil
	self.Dragging		= false
	
	setmetatable(self, DraggableObject)
	
	return self
end

-- Enables dragging
function DraggableObject:Enable()
	local object			= self.Object
	local dragInput			= nil
	local dragStart			= nil
	local startPos			= nil
	local preparingToDrag	= false
	
	-- Updates the element
	local function update(input)
		local delta 		= input.Position - dragStart
		local newPosition	= UDim2_new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y)
		object.Position 	= newPosition
	
		return newPosition
	end
	
	self.InputBegan = object.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			preparingToDrag = true

			local connection 
			connection = input.Changed:Connect(function()
				if input.UserInputState == Enum.UserInputState.End and (self.Dragging or preparingToDrag) then
					self.Dragging = false
					connection:Disconnect()
					
					if self.DragEnded and not preparingToDrag then
						self.DragEnded()
					end
					
					preparingToDrag = false
				end
			end)
		end
	end)
	
	self.InputChanged = object.InputChanged:Connect(function(input)
		if MouseOrTouchMovement(input) then
			dragInput = input
		end
	end)
	
	self.InputChanged2 = UserInputService.InputChanged:Connect(function(input)
		if object.Parent == nil then
			self:Disable()
			return
		end
		
		if MouseOrTouchMovement(input) and preparingToDrag then
			preparingToDrag = false
			
			if self.DragStarted then
				self.DragStarted()
			end
			
			self.Dragging	= true
			dragStart 		= input.Position
			startPos 		= object.Position
		end
		
		if input == dragInput and self.Dragging then
			local newPosition = update(input)
			
			if self.Dragged then
				self.Dragged(newPosition)
			end
		end
	end)
end

-- Disables dragging
function DraggableObject:Disable()
	self.InputBegan:Disconnect()
	self.InputChanged:Disconnect()
	self.InputChanged2:Disconnect()
	
	if self.Dragging then
		self.Dragging = false
		
		if self.DragEnded then
			self.DragEnded()
		end
	end
end

return DraggableObject

Other than that works like a charm, thank you @Spynaz

8 Likes

Great resource love this a ton.

Quick question, though?

Do you know how I could use this to only let users drag a certain distance and space?

Here is my idea -

image

Just to keep the little button they drag in the position of “White Speed Bar”.

So only certain distances.

I see you give the positions exactly but just curious.

3 Likes

This is Incredibly useful! Thanks!

Everything works perfectly on PC, however, mobile seems to bug out on this, here is a video explaining my situation. Is anyone able to help me out with this?

you have to download the fixed code by @homermafia1:

That partially fixes it, but there’s still an issue. Here’s a video.

Fixed it by adding a currentlyDragging variable.
DraggableObject.lua (3.1 KB)

1 Like

It’s because you’re disconnecting the draggable object before you’re enabling it.
self.InputBegan and the other variables haven’t been assigned a connection yet!

How would you make it so that when something is reparented (which then causes the position to be displaced), it doesn’t become offset from the mouse?
This is what’s happening with me:

This really solves the problem . Please let’s update marketplace model with this bugfix cause market model is outdated.

1 Like

I’m having an issue where it seems like DragEnded doesn’t fire when I let go of the mouse. For some reason that also means that the next segment of code doesn’t fire and the UI doesn’t return to its original position.

local function inventorySlotSelected(index, slotFrame, dragObject)
	local item = inventory.items[index]
	
	-- Check if double click
	if tick() - lastClickTick <= 0.35 then
		dragObject:Disable()
		if item then
			if character:FindFirstChild(item.Name) then
				InventoryController:UnequipTools()
				InventoryController:ToggleUI(false)
			else
				InventoryController:EquipTool(item)
				InventoryController:ToggleUI(false)
			end
		else
			InventoryController:UnequipTools()
			InventoryController:ToggleUI(false)
		end
	else
		dragObject.DragEnded = function()
			print("Drag Ended")
			if not collidesWith(slotFrame.TextButton, INVENTORY_UI:FindFirstChild("Slots")) then
				-- DROP
				print("Drop")
				return
			else
				-- Check If Collides With Other
				print("Swap")
				return
			end
		end
	end
	dragObject:Disable()
	lastClickTick = tick()
	slotFrame.TextButton.Position = UDim2.new(0,0,0,0)
	print("Move Back")
	dragObject:Enable()
end