Context Menu Maker

Create a drop down menu at your leisure and show it when you're ready. This is a very basic context menu, nothing advanced.
== Usage ==
local menu = MenuClass:New()
menu:AddItem('Do Something', function()
 SendChatMessage("Hello Bob!", "WHISPER", "Common", "Bob")
end)
menu:AddItem('Close', function()
 -- do nothing, just close.
end)
local f = SomeClickableFrame
f:SetScript('OnClick', function(self, button)
 menu:Show()
end)
== Properties ==
;.menuItems : Table. Stores the menu items.
;.anchor : String. The anchor.
;.x : The x offset
;.y : The y offset
;.displayMode : The display mode.
;.autoHideDelay : The time it takes for the menu to automatically hide.
;.menuFrame : The menu frame.
;.uniqueID : A self incrementing number used to ensure that the generic menu frame is unique.
;.isTitle : Is this menu item a header? 1 if yes, nil if no.
== Methods ==
;:Activate() : Creates the menu frame if it has not already been set with :SetMenuFrame().
;:AddItem(<string> text, <function> func, <number> isTitle) : Add a menu item with text text that does func. Requires at least 1.
;:GetAttribute(<string|number> text, <string|number> attribute) : Gets an attribute for the given menu item.
;:New() : Allocate memory for the new menu and set the defaults. Required. Returns the menu.
;:RemoveItem(<string> text) : Remove the first menu item that matches text
;:SetAnchor(<string> anchor) : Set the anchor. Optional. Default: "cursor"
;:SetAttribute(<string|number> text, <string|number> attribute, <string|number> value) : Sets an attribute for the given menu item.
;:SetDisplayMode(<string> displayMode) : Set the displayMode. Optional. Default: "MENU"
;:SetAutoHideDelay(<number> autoHideDelay) : Set the autoHideDelay. Optional. Default: nil (does not auto close)
;:SetMenuFrame(<menuFrame> menuFrame) : Set the menuFrame. Optional. Default: self generating menuFrame. If set, this must be a frame that inherits UIDropDownMenuTemplate.
;:SetX(<number> x[, <boolean> save]) : Set the x offset. Optional. Default: nil (0 if anchor is not "cursor"). If save resolves to true, x will be added to rather than replaced.
;:SetY(<number> y[, <boolean> save]) : Set the y offset. Optional. Default: nil (0 if anchor is not "cursor"). If save resolves to true, y will be added to rather than replaced.
;:Show() : Show the menu.
== Code ==
-- Set to false to use file-scoped variables or true to use the new addon-scoped variables
local useAddonScope = true
local addonName, MenuClass
if useAddonScope then
 addonName, MenuClass = ...
else
 addonName, MenuClass = "--your addon's name--", {}
end
function MenuClass:New()
 local ret = {}
 
 -- set the defaults
 ret.menuList = {}
 ret.anchor = 'cursor'; -- default at the cursor
 ret.x = nil;
 ret.y = nil;
 ret.displayMode = 'MENU'; -- default
 ret.autoHideDelay = nil;
 ret.menuFrame = nil; -- If not defined, :Show() will create a generic menu frame
 ret.uniqueID = 1
 -- import the functions
 for k,v in pairs(self) do
 ret[k] = v
 end
 
 -- return a copy of the class
 return ret
end
--[[
 Return the index where "text" lives.
 ; text : The text to search for.
--]]
function MenuClass:GetItemByText(text)
 for k,v in pairs(self.menuList) do
 if v.text == text then
 return k
 end
 end
end
--[[
 Add menu items
 ; text : The display text.
 ; func : The function to execute OnClick.
 ; isTitle : 1 if this is a header (usually the first one)
 ; otherAttributes : table - { ["attribute"] = value, }
 returns the last index of the menu item that was just added.
--]]
function MenuClass:AddItem(text, func, isTitle, otherAttributes)
 local info = {}
 
 info["text"] = text
 info["isTitle"] = isTitle
 info["func"] = func
 
 if type(otherAttributes) == "table" then
 for attribute, value in pairs(otherAttributes) do
 info[attribute] = value
 end
 end
 table.insert(self.menuList, info)
 return #self.menuList
end
--[[
 Set an attribute for the menu item.
 Valid attributes are found in the FrameXML\UIDropDownMenu.lua file with their valid values.
 Arbitrary non-official attributes are allowed, but are only useful if you plan to access them with :GetAttribute().
 ; text : The text of the menu item or index of the menu item.
 ; attribute : Set this attribute to "value".
 ; value : The value to set the attribute to.
--]]
function MenuClass:SetAttribute(text, attribute, value)
 self.menuList[self:GetItemByText(text) or (self.menuList[text] and text) or 1][attribute or "uniqueID"] = value
end
--[[
 Get an attribute for the menu item.
 Valid attributes are found in the FrameXML\UIDropDownMenu.lua file with their valid values or any arbitrary attribute set with :SetAttribute().
 ; text : The text of the menu item or index of the menu item.
 ; attribute : Get this attribute.
--]]
function MenuClass:GetAttribute(text, attribute)
 return self.menuList[self:GetItemByText(text) or (self.menuList[text] and text) or 1][attribute or "uniqueID"]
end
--[[
 Remove the first item matching "text"
 ; text : The text to search for.
--]]
function MenuClass:RemoveItem(text)
 table.remove(self.menuList, self:GetItemByText(text))
end
--[[
 ; anchor : Set the anchor point. 
--]]
function MenuClass:SetAnchor(anchor)
 if anchor ~= 'cursor' then
 self.x = 0
 self.y = 0
 end
 self.anchor = anchor
end
--[[
 ; displayMode : "MENU"
--]]
function MenuClass:SetDisplayMode(displayMode)
 self.displayMode = displayMode
end
--[[
 ; autoHideDelay : How long, without a click, before the menu goes away.
--]]
function MenuClass:SetAutoHideDelay(autoHideDelay)
 self.autoHideDelay = tonumber(autoHideDelay)
end
--[[
 ; menuFrame : Should inherit a Drop Down Menu template.
--]]
function MenuClass:SetMenuFrame(menuFrame)
 self.menuFrame = menuFrame
end
function MenuClass:GetMenuList()
 return self.menuList
end
--[[
 ; x : X position
 ; save : When not nil, will add to the current value rather than replace it
--]]
function MenuClass:SetX(x, save)
 if save then
 self.x = self.x + x
 else
 self.x = x
 end
end
--[[
 ; y : Y position
 ; save : When not nil, will add to the current value rather than replace it
--]]
function MenuClass:SetY(y, save)
 if save then
 self.y = self.y + y
 else
 self.y = y
 end
end
function MenuClass:Activate()
 if not self.menuFrame then
 while _G['GenericMenuClassFrame'..self.uniqueID] do -- ensure that there's no namespace collisions
 self.uniqueID = self.uniqueID + 1
 end
 -- the frame must be named for some reason
 self.menuFrame = CreateFrame('Frame', 'GenericMenuClassFrame'..self.uniqueID, UIParent, "UIDropDownMenuTemplate")
 end
 self.menuFrame.menuList = self.menuList
end
--[[
 Show the menu.
--]]
function MenuClass:Show()
 self:Activate()
 EasyMenu(self.menuList, self.menuFrame, self.anchor, self.x, self.y, self.displayMode, self.autoHideDelay)
end
-- If you're not using the addon-scoped variables, you must have a global variable in order to use this menu.
if not useAddonScope then
 _G[addonName.."Menu"] = MenuClass
end