diff --git a/common/menu_store.go b/common/menu_store.go
index c36a6c36..a706be1c 100644
--- a/common/menu_store.go
+++ b/common/menu_store.go
@@ -59,7 +59,7 @@ func (store *DefaultMenuStore) Load(mid int) error {
if err != nil {
return err
}
- hold := &MenuListHolder{mlist, make(map[int]menuTmpl)}
+ hold := &MenuListHolder{mid, mlist, make(map[int]menuTmpl)}
err = hold.Preparse()
if err != nil {
return err
diff --git a/common/menus.go b/common/menus.go
index db1f0d80..0297694c 100644
--- a/common/menus.go
+++ b/common/menus.go
@@ -14,6 +14,7 @@ import (
type MenuItemList []MenuItem
type MenuListHolder struct {
+ MenuID int
List MenuItemList
Variations map[int]menuTmpl // 0 = Guest Menu, 1 = Member Menu, 2 = Super Mod Menu, 3 = Admin Menu
}
@@ -43,8 +44,12 @@ type MenuItem struct {
AdminOnly bool
}
+// TODO: Move the menu item stuff to it's own file
type MenuItemStmts struct {
- update *sql.Stmt
+ update *sql.Stmt
+ insert *sql.Stmt
+ delete *sql.Stmt
+ updateOrder *sql.Stmt
}
var menuItemStmts MenuItemStmts
@@ -52,7 +57,10 @@ var menuItemStmts MenuItemStmts
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
menuItemStmts = MenuItemStmts{
- update: acc.Update("menu_items").Set("name = ?, htmlID = ?, cssClass = ?, position = ?, path = ?, aria = ?, tooltip = ?, tmplName = ?, guestOnly = ?, memberOnly = ?, staffOnly = ?, adminOnly = ?").Where("miid = ?").Prepare(),
+ update: acc.Update("menu_items").Set("name = ?, htmlID = ?, cssClass = ?, position = ?, path = ?, aria = ?, tooltip = ?, tmplName = ?, guestOnly = ?, memberOnly = ?, staffOnly = ?, adminOnly = ?").Where("miid = ?").Prepare(),
+ insert: acc.Insert("menu_items").Columns("mid, name, htmlID, cssClass, position, path, aria, tooltip, tmplName, guestOnly, memberOnly, staffOnly, adminOnly").Fields("?,?,?,?,?,?,?,?,?,?,?,?,?").Prepare(),
+ delete: acc.Delete("menu_items").Where("miid = ?").Prepare(),
+ updateOrder: acc.Update("menu_items").Set("order = ?").Where("miid = ?").Prepare(),
}
return acc.FirstError()
})
@@ -64,6 +72,23 @@ func (item MenuItem) Commit() error {
return err
}
+func (item MenuItem) Create() (int, error) {
+ res, err := menuItemStmts.insert.Exec(item.MenuID, item.Name, item.HTMLID, item.CSSClass, item.Position, item.Path, item.Aria, item.Tooltip, item.TmplName, item.GuestOnly, item.MemberOnly, item.SuperModOnly, item.AdminOnly)
+ if err != nil {
+ return 0, err
+ }
+ Menus.Load(item.MenuID)
+
+ miid64, err := res.LastInsertId()
+ return int(miid64), err
+}
+
+func (item MenuItem) Delete() error {
+ _, err := menuItemStmts.delete.Exec(item.ID)
+ Menus.Load(item.MenuID)
+ return err
+}
+
func (hold *MenuListHolder) LoadTmpl(name string) (menuTmpl MenuTmpl, err error) {
data, err := ioutil.ReadFile("./templates/" + name + ".html")
if err != nil {
@@ -72,6 +97,18 @@ func (hold *MenuListHolder) LoadTmpl(name string) (menuTmpl MenuTmpl, err error)
return hold.Parse(name, data), nil
}
+// TODO: Make this atomic, maybe with a transaction or store the order on the menu itself?
+func (hold *MenuListHolder) UpdateOrder(updateMap map[int]int) error {
+ for miid, order := range updateMap {
+ _, err := menuItemStmts.updateOrder.Exec(order, miid)
+ if err != nil {
+ return err
+ }
+ }
+ Menus.Load(hold.MenuID)
+ return nil
+}
+
func (hold *MenuListHolder) LoadTmpls() (tmpls map[string]MenuTmpl, err error) {
tmpls = make(map[string]MenuTmpl)
var loadTmpl = func(name string) error {
diff --git a/common/pages.go b/common/pages.go
index 9c920ada..ca55505c 100644
--- a/common/pages.go
+++ b/common/pages.go
@@ -276,7 +276,7 @@ type PanelMenuPage struct {
*Header
Stats PanelStats
Zone string
- ID int
+ MenuID int
ItemList []MenuItem
}
diff --git a/common/user.go b/common/user.go
index 556ac2a4..0b8b0492 100644
--- a/common/user.go
+++ b/common/user.go
@@ -1,7 +1,7 @@
/*
*
* Gosora User File
-* Copyright Azareal 2017 - 2018
+* Copyright Azareal 2017 - 2019
*
*/
package common
@@ -88,7 +88,7 @@ func init() {
activate: acc.SimpleUpdate("users", "active = 1", where),
changeGroup: acc.SimpleUpdate("users", "group = ?", where), // TODO: Implement user_count for users_groups here
delete: acc.SimpleDelete("users", where),
- setAvatar: acc.SimpleUpdate("users", "avatar = ?", where),
+ setAvatar: acc.Update("users").Set("avatar = ?").Where(where).Prepare(),
setUsername: acc.Update("users").Set("name = ?").Where(where).Prepare(),
incrementTopics: acc.SimpleUpdate("users", "topics = topics + ?", where),
updateLevel: acc.SimpleUpdate("users", "level = ?", where),
@@ -101,7 +101,7 @@ func init() {
//recalcLastLiked: acc...
updateLastIP: acc.SimpleUpdate("users", "last_ip = ?", where),
- setPassword: acc.SimpleUpdate("users", "password = ?, salt = ?", where),
+ setPassword: acc.Update("users").Set("password = ?, salt = ?").Where(where).Prepare(),
}
return acc.FirstError()
})
@@ -244,9 +244,7 @@ func (user *User) ChangeName(username string) (err error) {
}
func (user *User) ChangeAvatar(avatar string) (err error) {
- _, err = userStmts.setAvatar.Exec(avatar, user.ID)
- user.CacheRemove()
- return err
+ return user.bindStmt(userStmts.setAvatar, avatar)
}
func (user *User) ChangeGroup(group int) (err error) {
diff --git a/config_default.noparse b/config_default.noparse
index 3203f775..dc8ac451 100644
--- a/config_default.noparse
+++ b/config_default.noparse
@@ -1,8 +1,8 @@
package main
-import "./common"
+import "../common"
-func init() {
+func Config() {
// Site Info
common.Site.ShortName = "Ts" // This should be less than three letters to fit in the navbar
common.Site.Name = "Test Site"
diff --git a/gen_router.go b/gen_router.go
index 4c5f0015..23427049 100644
--- a/gen_router.go
+++ b/gen_router.go
@@ -53,6 +53,9 @@ var RouteMap = map[string]interface{}{
"routePanelThemesMenusEdit": routePanelThemesMenusEdit,
"routePanelThemesMenuItemEdit": routePanelThemesMenuItemEdit,
"routePanelThemesMenuItemEditSubmit": routePanelThemesMenuItemEditSubmit,
+ "routePanelThemesMenuItemCreateSubmit": routePanelThemesMenuItemCreateSubmit,
+ "routePanelThemesMenuItemDeleteSubmit": routePanelThemesMenuItemDeleteSubmit,
+ "routePanelThemesMenuItemOrderSubmit": routePanelThemesMenuItemOrderSubmit,
"routePanelPlugins": routePanelPlugins,
"routePanelPluginsActivate": routePanelPluginsActivate,
"routePanelPluginsDeactivate": routePanelPluginsDeactivate,
@@ -166,81 +169,84 @@ var routeMapEnum = map[string]int{
"routePanelThemesMenusEdit": 31,
"routePanelThemesMenuItemEdit": 32,
"routePanelThemesMenuItemEditSubmit": 33,
- "routePanelPlugins": 34,
- "routePanelPluginsActivate": 35,
- "routePanelPluginsDeactivate": 36,
- "routePanelPluginsInstall": 37,
- "routePanelUsers": 38,
- "routePanelUsersEdit": 39,
- "routePanelUsersEditSubmit": 40,
- "routePanelAnalyticsViews": 41,
- "routePanelAnalyticsRoutes": 42,
- "routePanelAnalyticsAgents": 43,
- "routePanelAnalyticsSystems": 44,
- "routePanelAnalyticsLanguages": 45,
- "routePanelAnalyticsReferrers": 46,
- "routePanelAnalyticsRouteViews": 47,
- "routePanelAnalyticsAgentViews": 48,
- "routePanelAnalyticsForumViews": 49,
- "routePanelAnalyticsSystemViews": 50,
- "routePanelAnalyticsLanguageViews": 51,
- "routePanelAnalyticsReferrerViews": 52,
- "routePanelAnalyticsPosts": 53,
- "routePanelAnalyticsTopics": 54,
- "routePanelAnalyticsForums": 55,
- "routePanelGroups": 56,
- "routePanelGroupsEdit": 57,
- "routePanelGroupsEditPerms": 58,
- "routePanelGroupsEditSubmit": 59,
- "routePanelGroupsEditPermsSubmit": 60,
- "routePanelGroupsCreateSubmit": 61,
- "routePanelBackups": 62,
- "routePanelLogsMod": 63,
- "routePanelDebug": 64,
- "routePanelDashboard": 65,
- "routes.AccountEditCritical": 66,
- "routeAccountEditCriticalSubmit": 67,
- "routeAccountEditAvatar": 68,
- "routeAccountEditAvatarSubmit": 69,
- "routeAccountEditUsername": 70,
- "routeAccountEditUsernameSubmit": 71,
- "routeAccountEditEmail": 72,
- "routeAccountEditEmailTokenSubmit": 73,
- "routes.ViewProfile": 74,
- "routes.BanUserSubmit": 75,
- "routes.UnbanUser": 76,
- "routes.ActivateUser": 77,
- "routes.IPSearch": 78,
- "routes.CreateTopicSubmit": 79,
- "routes.EditTopicSubmit": 80,
- "routes.DeleteTopicSubmit": 81,
- "routes.StickTopicSubmit": 82,
- "routes.UnstickTopicSubmit": 83,
- "routes.LockTopicSubmit": 84,
- "routes.UnlockTopicSubmit": 85,
- "routes.MoveTopicSubmit": 86,
- "routeLikeTopicSubmit": 87,
- "routes.ViewTopic": 88,
- "routes.CreateReplySubmit": 89,
- "routes.ReplyEditSubmit": 90,
- "routes.ReplyDeleteSubmit": 91,
- "routeReplyLikeSubmit": 92,
- "routeProfileReplyCreateSubmit": 93,
- "routes.ProfileReplyEditSubmit": 94,
- "routes.ProfileReplyDeleteSubmit": 95,
- "routes.PollVote": 96,
- "routes.PollResults": 97,
- "routes.AccountLogin": 98,
- "routes.AccountRegister": 99,
- "routeLogout": 100,
- "routes.AccountLoginSubmit": 101,
- "routes.AccountRegisterSubmit": 102,
- "routeDynamic": 103,
- "routeUploads": 104,
- "routes.StaticFile": 105,
- "routes.RobotsTxt": 106,
- "routes.SitemapXml": 107,
- "BadRoute": 108,
+ "routePanelThemesMenuItemCreateSubmit": 34,
+ "routePanelThemesMenuItemDeleteSubmit": 35,
+ "routePanelThemesMenuItemOrderSubmit": 36,
+ "routePanelPlugins": 37,
+ "routePanelPluginsActivate": 38,
+ "routePanelPluginsDeactivate": 39,
+ "routePanelPluginsInstall": 40,
+ "routePanelUsers": 41,
+ "routePanelUsersEdit": 42,
+ "routePanelUsersEditSubmit": 43,
+ "routePanelAnalyticsViews": 44,
+ "routePanelAnalyticsRoutes": 45,
+ "routePanelAnalyticsAgents": 46,
+ "routePanelAnalyticsSystems": 47,
+ "routePanelAnalyticsLanguages": 48,
+ "routePanelAnalyticsReferrers": 49,
+ "routePanelAnalyticsRouteViews": 50,
+ "routePanelAnalyticsAgentViews": 51,
+ "routePanelAnalyticsForumViews": 52,
+ "routePanelAnalyticsSystemViews": 53,
+ "routePanelAnalyticsLanguageViews": 54,
+ "routePanelAnalyticsReferrerViews": 55,
+ "routePanelAnalyticsPosts": 56,
+ "routePanelAnalyticsTopics": 57,
+ "routePanelAnalyticsForums": 58,
+ "routePanelGroups": 59,
+ "routePanelGroupsEdit": 60,
+ "routePanelGroupsEditPerms": 61,
+ "routePanelGroupsEditSubmit": 62,
+ "routePanelGroupsEditPermsSubmit": 63,
+ "routePanelGroupsCreateSubmit": 64,
+ "routePanelBackups": 65,
+ "routePanelLogsMod": 66,
+ "routePanelDebug": 67,
+ "routePanelDashboard": 68,
+ "routes.AccountEditCritical": 69,
+ "routeAccountEditCriticalSubmit": 70,
+ "routeAccountEditAvatar": 71,
+ "routeAccountEditAvatarSubmit": 72,
+ "routeAccountEditUsername": 73,
+ "routeAccountEditUsernameSubmit": 74,
+ "routeAccountEditEmail": 75,
+ "routeAccountEditEmailTokenSubmit": 76,
+ "routes.ViewProfile": 77,
+ "routes.BanUserSubmit": 78,
+ "routes.UnbanUser": 79,
+ "routes.ActivateUser": 80,
+ "routes.IPSearch": 81,
+ "routes.CreateTopicSubmit": 82,
+ "routes.EditTopicSubmit": 83,
+ "routes.DeleteTopicSubmit": 84,
+ "routes.StickTopicSubmit": 85,
+ "routes.UnstickTopicSubmit": 86,
+ "routes.LockTopicSubmit": 87,
+ "routes.UnlockTopicSubmit": 88,
+ "routes.MoveTopicSubmit": 89,
+ "routeLikeTopicSubmit": 90,
+ "routes.ViewTopic": 91,
+ "routes.CreateReplySubmit": 92,
+ "routes.ReplyEditSubmit": 93,
+ "routes.ReplyDeleteSubmit": 94,
+ "routeReplyLikeSubmit": 95,
+ "routeProfileReplyCreateSubmit": 96,
+ "routes.ProfileReplyEditSubmit": 97,
+ "routes.ProfileReplyDeleteSubmit": 98,
+ "routes.PollVote": 99,
+ "routes.PollResults": 100,
+ "routes.AccountLogin": 101,
+ "routes.AccountRegister": 102,
+ "routeLogout": 103,
+ "routes.AccountLoginSubmit": 104,
+ "routes.AccountRegisterSubmit": 105,
+ "routeDynamic": 106,
+ "routeUploads": 107,
+ "routes.StaticFile": 108,
+ "routes.RobotsTxt": 109,
+ "routes.SitemapXml": 110,
+ "BadRoute": 111,
}
var reverseRouteMapEnum = map[int]string{
0: "routeAPI",
@@ -277,81 +283,84 @@ var reverseRouteMapEnum = map[int]string{
31: "routePanelThemesMenusEdit",
32: "routePanelThemesMenuItemEdit",
33: "routePanelThemesMenuItemEditSubmit",
- 34: "routePanelPlugins",
- 35: "routePanelPluginsActivate",
- 36: "routePanelPluginsDeactivate",
- 37: "routePanelPluginsInstall",
- 38: "routePanelUsers",
- 39: "routePanelUsersEdit",
- 40: "routePanelUsersEditSubmit",
- 41: "routePanelAnalyticsViews",
- 42: "routePanelAnalyticsRoutes",
- 43: "routePanelAnalyticsAgents",
- 44: "routePanelAnalyticsSystems",
- 45: "routePanelAnalyticsLanguages",
- 46: "routePanelAnalyticsReferrers",
- 47: "routePanelAnalyticsRouteViews",
- 48: "routePanelAnalyticsAgentViews",
- 49: "routePanelAnalyticsForumViews",
- 50: "routePanelAnalyticsSystemViews",
- 51: "routePanelAnalyticsLanguageViews",
- 52: "routePanelAnalyticsReferrerViews",
- 53: "routePanelAnalyticsPosts",
- 54: "routePanelAnalyticsTopics",
- 55: "routePanelAnalyticsForums",
- 56: "routePanelGroups",
- 57: "routePanelGroupsEdit",
- 58: "routePanelGroupsEditPerms",
- 59: "routePanelGroupsEditSubmit",
- 60: "routePanelGroupsEditPermsSubmit",
- 61: "routePanelGroupsCreateSubmit",
- 62: "routePanelBackups",
- 63: "routePanelLogsMod",
- 64: "routePanelDebug",
- 65: "routePanelDashboard",
- 66: "routes.AccountEditCritical",
- 67: "routeAccountEditCriticalSubmit",
- 68: "routeAccountEditAvatar",
- 69: "routeAccountEditAvatarSubmit",
- 70: "routeAccountEditUsername",
- 71: "routeAccountEditUsernameSubmit",
- 72: "routeAccountEditEmail",
- 73: "routeAccountEditEmailTokenSubmit",
- 74: "routes.ViewProfile",
- 75: "routes.BanUserSubmit",
- 76: "routes.UnbanUser",
- 77: "routes.ActivateUser",
- 78: "routes.IPSearch",
- 79: "routes.CreateTopicSubmit",
- 80: "routes.EditTopicSubmit",
- 81: "routes.DeleteTopicSubmit",
- 82: "routes.StickTopicSubmit",
- 83: "routes.UnstickTopicSubmit",
- 84: "routes.LockTopicSubmit",
- 85: "routes.UnlockTopicSubmit",
- 86: "routes.MoveTopicSubmit",
- 87: "routeLikeTopicSubmit",
- 88: "routes.ViewTopic",
- 89: "routes.CreateReplySubmit",
- 90: "routes.ReplyEditSubmit",
- 91: "routes.ReplyDeleteSubmit",
- 92: "routeReplyLikeSubmit",
- 93: "routeProfileReplyCreateSubmit",
- 94: "routes.ProfileReplyEditSubmit",
- 95: "routes.ProfileReplyDeleteSubmit",
- 96: "routes.PollVote",
- 97: "routes.PollResults",
- 98: "routes.AccountLogin",
- 99: "routes.AccountRegister",
- 100: "routeLogout",
- 101: "routes.AccountLoginSubmit",
- 102: "routes.AccountRegisterSubmit",
- 103: "routeDynamic",
- 104: "routeUploads",
- 105: "routes.StaticFile",
- 106: "routes.RobotsTxt",
- 107: "routes.SitemapXml",
- 108: "BadRoute",
+ 34: "routePanelThemesMenuItemCreateSubmit",
+ 35: "routePanelThemesMenuItemDeleteSubmit",
+ 36: "routePanelThemesMenuItemOrderSubmit",
+ 37: "routePanelPlugins",
+ 38: "routePanelPluginsActivate",
+ 39: "routePanelPluginsDeactivate",
+ 40: "routePanelPluginsInstall",
+ 41: "routePanelUsers",
+ 42: "routePanelUsersEdit",
+ 43: "routePanelUsersEditSubmit",
+ 44: "routePanelAnalyticsViews",
+ 45: "routePanelAnalyticsRoutes",
+ 46: "routePanelAnalyticsAgents",
+ 47: "routePanelAnalyticsSystems",
+ 48: "routePanelAnalyticsLanguages",
+ 49: "routePanelAnalyticsReferrers",
+ 50: "routePanelAnalyticsRouteViews",
+ 51: "routePanelAnalyticsAgentViews",
+ 52: "routePanelAnalyticsForumViews",
+ 53: "routePanelAnalyticsSystemViews",
+ 54: "routePanelAnalyticsLanguageViews",
+ 55: "routePanelAnalyticsReferrerViews",
+ 56: "routePanelAnalyticsPosts",
+ 57: "routePanelAnalyticsTopics",
+ 58: "routePanelAnalyticsForums",
+ 59: "routePanelGroups",
+ 60: "routePanelGroupsEdit",
+ 61: "routePanelGroupsEditPerms",
+ 62: "routePanelGroupsEditSubmit",
+ 63: "routePanelGroupsEditPermsSubmit",
+ 64: "routePanelGroupsCreateSubmit",
+ 65: "routePanelBackups",
+ 66: "routePanelLogsMod",
+ 67: "routePanelDebug",
+ 68: "routePanelDashboard",
+ 69: "routes.AccountEditCritical",
+ 70: "routeAccountEditCriticalSubmit",
+ 71: "routeAccountEditAvatar",
+ 72: "routeAccountEditAvatarSubmit",
+ 73: "routeAccountEditUsername",
+ 74: "routeAccountEditUsernameSubmit",
+ 75: "routeAccountEditEmail",
+ 76: "routeAccountEditEmailTokenSubmit",
+ 77: "routes.ViewProfile",
+ 78: "routes.BanUserSubmit",
+ 79: "routes.UnbanUser",
+ 80: "routes.ActivateUser",
+ 81: "routes.IPSearch",
+ 82: "routes.CreateTopicSubmit",
+ 83: "routes.EditTopicSubmit",
+ 84: "routes.DeleteTopicSubmit",
+ 85: "routes.StickTopicSubmit",
+ 86: "routes.UnstickTopicSubmit",
+ 87: "routes.LockTopicSubmit",
+ 88: "routes.UnlockTopicSubmit",
+ 89: "routes.MoveTopicSubmit",
+ 90: "routeLikeTopicSubmit",
+ 91: "routes.ViewTopic",
+ 92: "routes.CreateReplySubmit",
+ 93: "routes.ReplyEditSubmit",
+ 94: "routes.ReplyDeleteSubmit",
+ 95: "routeReplyLikeSubmit",
+ 96: "routeProfileReplyCreateSubmit",
+ 97: "routes.ProfileReplyEditSubmit",
+ 98: "routes.ProfileReplyDeleteSubmit",
+ 99: "routes.PollVote",
+ 100: "routes.PollResults",
+ 101: "routes.AccountLogin",
+ 102: "routes.AccountRegister",
+ 103: "routeLogout",
+ 104: "routes.AccountLoginSubmit",
+ 105: "routes.AccountRegisterSubmit",
+ 106: "routeDynamic",
+ 107: "routeUploads",
+ 108: "routes.StaticFile",
+ 109: "routes.RobotsTxt",
+ 110: "routes.SitemapXml",
+ 111: "BadRoute",
}
var osMapEnum = map[string]int{
"unknown": 0,
@@ -647,7 +656,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
counters.GlobalViewCounter.Bump()
if prefix == "/static" {
- counters.RouteViewCounter.Bump(105)
+ counters.RouteViewCounter.Bump(108)
req.URL.Path += extraData
routes.StaticFile(w, req)
return
@@ -1068,8 +1077,35 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
counters.RouteViewCounter.Bump(33)
err = routePanelThemesMenuItemEditSubmit(w,req,user,extraData)
- case "/panel/plugins/":
+ case "/panel/themes/menus/item/create/submit/":
+ err = common.NoSessionMismatch(w,req,user)
+ if err != nil {
+ router.handleError(err,w,req,user)
+ return
+ }
+
counters.RouteViewCounter.Bump(34)
+ err = routePanelThemesMenuItemCreateSubmit(w,req,user)
+ case "/panel/themes/menus/item/delete/submit/":
+ err = common.NoSessionMismatch(w,req,user)
+ if err != nil {
+ router.handleError(err,w,req,user)
+ return
+ }
+
+ counters.RouteViewCounter.Bump(35)
+ err = routePanelThemesMenuItemDeleteSubmit(w,req,user,extraData)
+ case "/panel/themes/menus/item/order/edit/submit/":
+ err = common.NoSessionMismatch(w,req,user)
+ if err != nil {
+ router.handleError(err,w,req,user)
+ return
+ }
+
+ counters.RouteViewCounter.Bump(36)
+ err = routePanelThemesMenuItemOrderSubmit(w,req,user,extraData)
+ case "/panel/plugins/":
+ counters.RouteViewCounter.Bump(37)
err = routePanelPlugins(w,req,user)
case "/panel/plugins/activate/":
err = common.NoSessionMismatch(w,req,user)
@@ -1078,7 +1114,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(35)
+ counters.RouteViewCounter.Bump(38)
err = routePanelPluginsActivate(w,req,user,extraData)
case "/panel/plugins/deactivate/":
err = common.NoSessionMismatch(w,req,user)
@@ -1087,7 +1123,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(36)
+ counters.RouteViewCounter.Bump(39)
err = routePanelPluginsDeactivate(w,req,user,extraData)
case "/panel/plugins/install/":
err = common.NoSessionMismatch(w,req,user)
@@ -1096,13 +1132,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(37)
+ counters.RouteViewCounter.Bump(40)
err = routePanelPluginsInstall(w,req,user,extraData)
case "/panel/users/":
- counters.RouteViewCounter.Bump(38)
+ counters.RouteViewCounter.Bump(41)
err = routePanelUsers(w,req,user)
case "/panel/users/edit/":
- counters.RouteViewCounter.Bump(39)
+ counters.RouteViewCounter.Bump(42)
err = routePanelUsersEdit(w,req,user,extraData)
case "/panel/users/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1111,7 +1147,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(40)
+ counters.RouteViewCounter.Bump(43)
err = routePanelUsersEditSubmit(w,req,user,extraData)
case "/panel/analytics/views/":
err = common.ParseForm(w,req,user)
@@ -1120,7 +1156,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(41)
+ counters.RouteViewCounter.Bump(44)
err = routePanelAnalyticsViews(w,req,user)
case "/panel/analytics/routes/":
err = common.ParseForm(w,req,user)
@@ -1129,7 +1165,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(42)
+ counters.RouteViewCounter.Bump(45)
err = routePanelAnalyticsRoutes(w,req,user)
case "/panel/analytics/agents/":
err = common.ParseForm(w,req,user)
@@ -1138,7 +1174,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(43)
+ counters.RouteViewCounter.Bump(46)
err = routePanelAnalyticsAgents(w,req,user)
case "/panel/analytics/systems/":
err = common.ParseForm(w,req,user)
@@ -1147,7 +1183,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(44)
+ counters.RouteViewCounter.Bump(47)
err = routePanelAnalyticsSystems(w,req,user)
case "/panel/analytics/langs/":
err = common.ParseForm(w,req,user)
@@ -1156,7 +1192,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(45)
+ counters.RouteViewCounter.Bump(48)
err = routePanelAnalyticsLanguages(w,req,user)
case "/panel/analytics/referrers/":
err = common.ParseForm(w,req,user)
@@ -1165,25 +1201,25 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(46)
+ counters.RouteViewCounter.Bump(49)
err = routePanelAnalyticsReferrers(w,req,user)
case "/panel/analytics/route/":
- counters.RouteViewCounter.Bump(47)
+ counters.RouteViewCounter.Bump(50)
err = routePanelAnalyticsRouteViews(w,req,user,extraData)
case "/panel/analytics/agent/":
- counters.RouteViewCounter.Bump(48)
+ counters.RouteViewCounter.Bump(51)
err = routePanelAnalyticsAgentViews(w,req,user,extraData)
case "/panel/analytics/forum/":
- counters.RouteViewCounter.Bump(49)
+ counters.RouteViewCounter.Bump(52)
err = routePanelAnalyticsForumViews(w,req,user,extraData)
case "/panel/analytics/system/":
- counters.RouteViewCounter.Bump(50)
+ counters.RouteViewCounter.Bump(53)
err = routePanelAnalyticsSystemViews(w,req,user,extraData)
case "/panel/analytics/lang/":
- counters.RouteViewCounter.Bump(51)
+ counters.RouteViewCounter.Bump(54)
err = routePanelAnalyticsLanguageViews(w,req,user,extraData)
case "/panel/analytics/referrer/":
- counters.RouteViewCounter.Bump(52)
+ counters.RouteViewCounter.Bump(55)
err = routePanelAnalyticsReferrerViews(w,req,user,extraData)
case "/panel/analytics/posts/":
err = common.ParseForm(w,req,user)
@@ -1192,7 +1228,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(53)
+ counters.RouteViewCounter.Bump(56)
err = routePanelAnalyticsPosts(w,req,user)
case "/panel/analytics/topics/":
err = common.ParseForm(w,req,user)
@@ -1201,7 +1237,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(54)
+ counters.RouteViewCounter.Bump(57)
err = routePanelAnalyticsTopics(w,req,user)
case "/panel/analytics/forums/":
err = common.ParseForm(w,req,user)
@@ -1210,16 +1246,16 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(55)
+ counters.RouteViewCounter.Bump(58)
err = routePanelAnalyticsForums(w,req,user)
case "/panel/groups/":
- counters.RouteViewCounter.Bump(56)
+ counters.RouteViewCounter.Bump(59)
err = routePanelGroups(w,req,user)
case "/panel/groups/edit/":
- counters.RouteViewCounter.Bump(57)
+ counters.RouteViewCounter.Bump(60)
err = routePanelGroupsEdit(w,req,user,extraData)
case "/panel/groups/edit/perms/":
- counters.RouteViewCounter.Bump(58)
+ counters.RouteViewCounter.Bump(61)
err = routePanelGroupsEditPerms(w,req,user,extraData)
case "/panel/groups/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1228,7 +1264,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(59)
+ counters.RouteViewCounter.Bump(62)
err = routePanelGroupsEditSubmit(w,req,user,extraData)
case "/panel/groups/edit/perms/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1237,7 +1273,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(60)
+ counters.RouteViewCounter.Bump(63)
err = routePanelGroupsEditPermsSubmit(w,req,user,extraData)
case "/panel/groups/create/":
err = common.NoSessionMismatch(w,req,user)
@@ -1246,7 +1282,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(61)
+ counters.RouteViewCounter.Bump(64)
err = routePanelGroupsCreateSubmit(w,req,user)
case "/panel/backups/":
err = common.SuperAdminOnly(w,req,user)
@@ -1255,10 +1291,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(62)
+ counters.RouteViewCounter.Bump(65)
err = routePanelBackups(w,req,user,extraData)
case "/panel/logs/mod/":
- counters.RouteViewCounter.Bump(63)
+ counters.RouteViewCounter.Bump(66)
err = routePanelLogsMod(w,req,user)
case "/panel/debug/":
err = common.AdminOnly(w,req,user)
@@ -1267,10 +1303,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(64)
+ counters.RouteViewCounter.Bump(67)
err = routePanelDebug(w,req,user)
default:
- counters.RouteViewCounter.Bump(65)
+ counters.RouteViewCounter.Bump(68)
err = routePanelDashboard(w,req,user)
}
if err != nil {
@@ -1285,7 +1321,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(66)
+ counters.RouteViewCounter.Bump(69)
err = routes.AccountEditCritical(w,req,user)
case "/user/edit/critical/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1300,7 +1336,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(67)
+ counters.RouteViewCounter.Bump(70)
err = routeAccountEditCriticalSubmit(w,req,user)
case "/user/edit/avatar/":
err = common.MemberOnly(w,req,user)
@@ -1309,7 +1345,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(68)
+ counters.RouteViewCounter.Bump(71)
err = routeAccountEditAvatar(w,req,user)
case "/user/edit/avatar/submit/":
err = common.MemberOnly(w,req,user)
@@ -1329,7 +1365,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(69)
+ counters.RouteViewCounter.Bump(72)
err = routeAccountEditAvatarSubmit(w,req,user)
case "/user/edit/username/":
err = common.MemberOnly(w,req,user)
@@ -1338,7 +1374,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(70)
+ counters.RouteViewCounter.Bump(73)
err = routeAccountEditUsername(w,req,user)
case "/user/edit/username/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1353,7 +1389,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(71)
+ counters.RouteViewCounter.Bump(74)
err = routeAccountEditUsernameSubmit(w,req,user)
case "/user/edit/email/":
err = common.MemberOnly(w,req,user)
@@ -1362,7 +1398,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(72)
+ counters.RouteViewCounter.Bump(75)
err = routeAccountEditEmail(w,req,user)
case "/user/edit/token/":
err = common.NoSessionMismatch(w,req,user)
@@ -1377,11 +1413,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(73)
+ counters.RouteViewCounter.Bump(76)
err = routeAccountEditEmailTokenSubmit(w,req,user,extraData)
default:
req.URL.Path += extraData
- counters.RouteViewCounter.Bump(74)
+ counters.RouteViewCounter.Bump(77)
err = routes.ViewProfile(w,req,user)
}
if err != nil {
@@ -1402,7 +1438,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(75)
+ counters.RouteViewCounter.Bump(78)
err = routes.BanUserSubmit(w,req,user,extraData)
case "/users/unban/":
err = common.NoSessionMismatch(w,req,user)
@@ -1417,7 +1453,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(76)
+ counters.RouteViewCounter.Bump(79)
err = routes.UnbanUser(w,req,user,extraData)
case "/users/activate/":
err = common.NoSessionMismatch(w,req,user)
@@ -1432,7 +1468,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(77)
+ counters.RouteViewCounter.Bump(80)
err = routes.ActivateUser(w,req,user,extraData)
case "/users/ips/":
err = common.MemberOnly(w,req,user)
@@ -1441,7 +1477,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(78)
+ counters.RouteViewCounter.Bump(81)
err = routes.IPSearch(w,req,user)
}
if err != nil {
@@ -1467,7 +1503,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(79)
+ counters.RouteViewCounter.Bump(82)
err = routes.CreateTopicSubmit(w,req,user)
case "/topic/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1482,7 +1518,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(80)
+ counters.RouteViewCounter.Bump(83)
err = routes.EditTopicSubmit(w,req,user,extraData)
case "/topic/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1498,7 +1534,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
req.URL.Path += extraData
- counters.RouteViewCounter.Bump(81)
+ counters.RouteViewCounter.Bump(84)
err = routes.DeleteTopicSubmit(w,req,user)
case "/topic/stick/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1513,7 +1549,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(82)
+ counters.RouteViewCounter.Bump(85)
err = routes.StickTopicSubmit(w,req,user,extraData)
case "/topic/unstick/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1528,7 +1564,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(83)
+ counters.RouteViewCounter.Bump(86)
err = routes.UnstickTopicSubmit(w,req,user,extraData)
case "/topic/lock/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1544,7 +1580,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
req.URL.Path += extraData
- counters.RouteViewCounter.Bump(84)
+ counters.RouteViewCounter.Bump(87)
err = routes.LockTopicSubmit(w,req,user)
case "/topic/unlock/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1559,7 +1595,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(85)
+ counters.RouteViewCounter.Bump(88)
err = routes.UnlockTopicSubmit(w,req,user,extraData)
case "/topic/move/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1574,7 +1610,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(86)
+ counters.RouteViewCounter.Bump(89)
err = routes.MoveTopicSubmit(w,req,user,extraData)
case "/topic/like/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1595,10 +1631,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(87)
+ counters.RouteViewCounter.Bump(90)
err = routeLikeTopicSubmit(w,req,user,extraData)
default:
- counters.RouteViewCounter.Bump(88)
+ counters.RouteViewCounter.Bump(91)
err = routes.ViewTopic(w,req,user, extraData)
}
if err != nil {
@@ -1624,7 +1660,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(89)
+ counters.RouteViewCounter.Bump(92)
err = routes.CreateReplySubmit(w,req,user)
case "/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1639,7 +1675,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(90)
+ counters.RouteViewCounter.Bump(93)
err = routes.ReplyEditSubmit(w,req,user,extraData)
case "/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1654,7 +1690,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(91)
+ counters.RouteViewCounter.Bump(94)
err = routes.ReplyDeleteSubmit(w,req,user,extraData)
case "/reply/like/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1675,7 +1711,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(92)
+ counters.RouteViewCounter.Bump(95)
err = routeReplyLikeSubmit(w,req,user,extraData)
}
if err != nil {
@@ -1696,7 +1732,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(93)
+ counters.RouteViewCounter.Bump(96)
err = routeProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1711,7 +1747,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(94)
+ counters.RouteViewCounter.Bump(97)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/":
err = common.NoSessionMismatch(w,req,user)
@@ -1726,7 +1762,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(95)
+ counters.RouteViewCounter.Bump(98)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
}
if err != nil {
@@ -1747,10 +1783,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(96)
+ counters.RouteViewCounter.Bump(99)
err = routes.PollVote(w,req,user,extraData)
case "/poll/results/":
- counters.RouteViewCounter.Bump(97)
+ counters.RouteViewCounter.Bump(100)
err = routes.PollResults(w,req,user,extraData)
}
if err != nil {
@@ -1759,10 +1795,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/accounts":
switch(req.URL.Path) {
case "/accounts/login/":
- counters.RouteViewCounter.Bump(98)
+ counters.RouteViewCounter.Bump(101)
err = routes.AccountLogin(w,req,user)
case "/accounts/create/":
- counters.RouteViewCounter.Bump(99)
+ counters.RouteViewCounter.Bump(102)
err = routes.AccountRegister(w,req,user)
case "/accounts/logout/":
err = common.NoSessionMismatch(w,req,user)
@@ -1777,7 +1813,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(100)
+ counters.RouteViewCounter.Bump(103)
err = routeLogout(w,req,user)
case "/accounts/login/submit/":
err = common.ParseForm(w,req,user)
@@ -1786,7 +1822,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(101)
+ counters.RouteViewCounter.Bump(104)
err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/create/submit/":
err = common.ParseForm(w,req,user)
@@ -1795,7 +1831,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
- counters.RouteViewCounter.Bump(102)
+ counters.RouteViewCounter.Bump(105)
err = routes.AccountRegisterSubmit(w,req,user)
}
if err != nil {
@@ -1812,7 +1848,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
common.NotFound(w,req,nil)
return
}
- counters.RouteViewCounter.Bump(104)
+ counters.RouteViewCounter.Bump(107)
req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this?
router.UploadHandler(w,req) // TODO: Count these views
@@ -1821,14 +1857,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: Add support for favicons and robots.txt files
switch(extraData) {
case "robots.txt":
- counters.RouteViewCounter.Bump(106)
+ counters.RouteViewCounter.Bump(109)
err = routes.RobotsTxt(w,req)
if err != nil {
router.handleError(err,w,req,user)
}
return
/*case "sitemap.xml":
- counters.RouteViewCounter.Bump(107)
+ counters.RouteViewCounter.Bump(110)
err = routes.SitemapXml(w,req)
if err != nil {
router.handleError(err,w,req,user)
@@ -1857,7 +1893,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
router.RUnlock()
if ok {
- counters.RouteViewCounter.Bump(103) // TODO: Be more specific about *which* dynamic route it is
+ counters.RouteViewCounter.Bump(106) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData
err = handle(w,req,user)
if err != nil {
@@ -1872,7 +1908,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} else {
router.DumpRequest(req,"Bad Route")
}
- counters.RouteViewCounter.Bump(108)
+ counters.RouteViewCounter.Bump(111)
common.NotFound(w,req,nil)
}
}
diff --git a/langs/english.json b/langs/english.json
index 8092128e..29dfce23 100644
--- a/langs/english.json
+++ b/langs/english.json
@@ -701,15 +701,21 @@
"panel_themes_make_default":"Make Default",
"panel_themes_menus_head":"Menus",
+ "panel_themes_menus_item_edit_button_aria":"Edit menu item",
+ "panel_themes_menus_item_delete_button_aria":"Delete menu item",
"panel_themes_menus_edit_head":"Menu Editor",
+ "panel_themes_menus_create_head":"Create Menu Item",
"panel_themes_menus_name":"Name",
+ "panel_themes_menus_name_placeholder":"Item Name",
"panel_themes_menus_htmlid":"HTML ID",
"panel_themes_menus_cssclass":"CSS Class",
"panel_themes_menus_position":"Position",
"panel_themes_menus_path":"Path",
"panel_themes_menus_aria":"Aria",
+ "panel_themes_menus_aria_placeholder":"Short description for helping those with poor vision",
"panel_themes_menus_tooltip":"Tooltip",
+ "panel_themes_menus_tooltip_placeholder":"A tooltip shown when you hover over it",
"panel_themes_menus_tmplname":"Template",
"panel_themes_menus_permissions":"Who Can See",
"panel_themes_menus_everyone": "Everyone",
@@ -718,6 +724,7 @@
"panel_themes_menus_supermodonly":"Super Mods",
"panel_themes_menus_adminonly":"Admins",
"panel_themes_menus_edit_update_button":"Update",
+ "panel_themes_menus_create_button":"Create",
"panel_settings_head":"Settings",
"panel_setting_head":"Edit Setting",
diff --git a/panel_routes.go b/panel_routes.go
index 3c9d1e88..cbb510ba 100644
--- a/panel_routes.go
+++ b/panel_routes.go
@@ -2417,6 +2417,7 @@ func routePanelThemesMenusEdit(w http.ResponseWriter, r *http.Request, user comm
}
// TODO: Something like Menu #1 for the title?
header.Title = common.GetTitlePhrase("panel_themes_menus_edit")
+ header.AddScript("Sortable-1.4.0/Sortable.min.js")
mid, err := strconv.Atoi(smid)
if err != nil {
@@ -2481,29 +2482,7 @@ func routePanelThemesMenuItemEdit(w http.ResponseWriter, r *http.Request, user c
return panelRenderTemplate("panel_themes_menus_item_edit", w, r, user, &pi)
}
-func routePanelThemesMenuItemEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sitemID string) common.RouteError {
- _, ferr := common.SimplePanelUserCheck(w, r, &user)
- if ferr != nil {
- return ferr
- }
- isJs := (r.PostFormValue("js") == "1")
- if !user.Perms.ManageThemes {
- return common.NoPermissionsJSQ(w, r, user, isJs)
- }
-
- itemID, err := strconv.Atoi(sitemID)
- if err != nil {
- return common.LocalErrorJSQ("Invalid integer", w, r, user, isJs)
- }
-
- menuItem, err := common.Menus.ItemStore().Get(itemID)
- if err == ErrNoRows {
- return common.LocalErrorJSQ("This item doesn't exist.", w, r, user, isJs)
- } else if err != nil {
- return common.InternalErrorJSQ(err, w, r, isJs)
- }
- //menuItem = menuItem.Copy() // If we switch this for a pointer, we might need this as a scratchpad
-
+func routePanelThemesMenuItemSetters(r *http.Request, menuItem common.MenuItem) common.MenuItem {
var getItem = func(name string) string {
return html.EscapeString(strings.Replace(r.PostFormValue("item-"+name), "\n", "", -1))
}
@@ -2519,8 +2498,7 @@ func routePanelThemesMenuItemEditSubmit(w http.ResponseWriter, r *http.Request,
menuItem.Tooltip = getItem("tooltip")
menuItem.TmplName = getItem("tmplname")
- var perms = getItem("permissions")
- switch perms {
+ switch getItem("permissions") {
case "everyone":
menuItem.GuestOnly = false
menuItem.MemberOnly = false
@@ -2547,6 +2525,32 @@ func routePanelThemesMenuItemEditSubmit(w http.ResponseWriter, r *http.Request,
menuItem.SuperModOnly = true
menuItem.AdminOnly = true
}
+ return menuItem
+}
+
+func routePanelThemesMenuItemEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sitemID string) common.RouteError {
+ _, ferr := common.SimplePanelUserCheck(w, r, &user)
+ if ferr != nil {
+ return ferr
+ }
+ isJs := (r.PostFormValue("js") == "1")
+ if !user.Perms.ManageThemes {
+ return common.NoPermissionsJSQ(w, r, user, isJs)
+ }
+
+ itemID, err := strconv.Atoi(sitemID)
+ if err != nil {
+ return common.LocalErrorJSQ("Invalid integer", w, r, user, isJs)
+ }
+
+ menuItem, err := common.Menus.ItemStore().Get(itemID)
+ if err == ErrNoRows {
+ return common.LocalErrorJSQ("This item doesn't exist.", w, r, user, isJs)
+ } else if err != nil {
+ return common.InternalErrorJSQ(err, w, r, isJs)
+ }
+ //menuItem = menuItem.Copy() // If we switch this for a pointer, we might need this as a scratchpad
+ menuItem = routePanelThemesMenuItemSetters(r, menuItem)
err = menuItem.Commit()
if err != nil {
@@ -2555,6 +2559,100 @@ func routePanelThemesMenuItemEditSubmit(w http.ResponseWriter, r *http.Request,
return panelSuccessRedirect("/panel/themes/menus/item/edit/"+strconv.Itoa(itemID), w, r, isJs)
}
+func routePanelThemesMenuItemCreateSubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
+ _, ferr := common.SimplePanelUserCheck(w, r, &user)
+ if ferr != nil {
+ return ferr
+ }
+
+ isJs := (r.PostFormValue("js") == "1")
+ if !user.Perms.ManageThemes {
+ return common.NoPermissionsJSQ(w, r, user, isJs)
+ }
+ smenuID := r.PostFormValue("mid")
+ if smenuID == "" {
+ return common.LocalErrorJSQ("No menuID provided", w, r, user, isJs)
+ }
+ menuID, err := strconv.Atoi(smenuID)
+ if err != nil {
+ return common.LocalErrorJSQ("Invalid integer", w, r, user, isJs)
+ }
+
+ menuItem := common.MenuItem{MenuID: menuID}
+ menuItem = routePanelThemesMenuItemSetters(r, menuItem)
+ itemID, err := menuItem.Create()
+ if err != nil {
+ return common.InternalErrorJSQ(err, w, r, isJs)
+ }
+ return panelSuccessRedirect("/panel/themes/menus/item/edit/"+strconv.Itoa(itemID), w, r, isJs)
+}
+
+func routePanelThemesMenuItemDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.User, sitemID string) common.RouteError {
+ _, ferr := common.SimplePanelUserCheck(w, r, &user)
+ if ferr != nil {
+ return ferr
+ }
+ isJs := (r.PostFormValue("js") == "1")
+ if !user.Perms.ManageThemes {
+ return common.NoPermissionsJSQ(w, r, user, isJs)
+ }
+
+ itemID, err := strconv.Atoi(sitemID)
+ if err != nil {
+ return common.LocalErrorJSQ("Invalid integer", w, r, user, isJs)
+ }
+ menuItem, err := common.Menus.ItemStore().Get(itemID)
+ if err == ErrNoRows {
+ return common.LocalErrorJSQ("This item doesn't exist.", w, r, user, isJs)
+ } else if err != nil {
+ return common.InternalErrorJSQ(err, w, r, isJs)
+ }
+ //menuItem = menuItem.Copy() // If we switch this for a pointer, we might need this as a scratchpad
+
+ err = menuItem.Delete()
+ if err != nil {
+ return common.InternalErrorJSQ(err, w, r, isJs)
+ }
+ return panelSuccessRedirect("/panel/themes/menus/", w, r, isJs)
+}
+
+func routePanelThemesMenuItemOrderSubmit(w http.ResponseWriter, r *http.Request, user common.User, smid string) common.RouteError {
+ _, ferr := common.SimplePanelUserCheck(w, r, &user)
+ if ferr != nil {
+ return ferr
+ }
+ isJs := (r.PostFormValue("js") == "1")
+ if !user.Perms.ManageThemes {
+ return common.NoPermissionsJSQ(w, r, user, isJs)
+ }
+
+ mid, err := strconv.Atoi(smid)
+ if err != nil {
+ return common.LocalErrorJSQ("Invalid integer", w, r, user, isJs)
+ }
+ menuHold, err := common.Menus.Get(mid)
+ if err == ErrNoRows {
+ return common.LocalErrorJSQ("Can't find menu", w, r, user, isJs)
+ } else if err != nil {
+ return common.InternalErrorJSQ(err, w, r, isJs)
+ }
+
+ sitems := strings.TrimSuffix(strings.TrimPrefix(r.PostFormValue("items"), "{"), "}")
+ fmt.Printf("sitems: %+v\n", sitems)
+
+ var updateMap = make(map[int]int)
+ for index, smiid := range strings.Split(sitems, ",") {
+ miid, err := strconv.Atoi(smiid)
+ if err != nil {
+ return common.LocalErrorJSQ("Invalid integer in menu item list", w, r, user, isJs)
+ }
+ updateMap[miid] = index
+ }
+ menuHold.UpdateOrder(updateMap)
+
+ return panelSuccessRedirect("/panel/themes/menus/edit/"+strconv.Itoa(mid), w, r, isJs)
+}
+
func routePanelBackups(w http.ResponseWriter, r *http.Request, user common.User, backupURL string) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
diff --git a/public/Sortable-1.4.0/.editorconfig b/public/Sortable-1.4.0/.editorconfig
new file mode 100644
index 00000000..b0d7fd91
--- /dev/null
+++ b/public/Sortable-1.4.0/.editorconfig
@@ -0,0 +1,12 @@
+# editorconfig.org
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/public/Sortable-1.4.0/.gitignore b/public/Sortable-1.4.0/.gitignore
new file mode 100644
index 00000000..434cfa9b
--- /dev/null
+++ b/public/Sortable-1.4.0/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+mock.png
+.*.sw*
+.build*
+jquery.fn.*
diff --git a/public/Sortable-1.4.0/.jshintrc b/public/Sortable-1.4.0/.jshintrc
new file mode 100644
index 00000000..3f67a098
--- /dev/null
+++ b/public/Sortable-1.4.0/.jshintrc
@@ -0,0 +1,24 @@
+{
+ "strict": true,
+ "newcap": false,
+ "node": true,
+ "expr": true,
+ "supernew": true,
+ "laxbreak": true,
+ "white": true,
+ "globals": {
+ "define": true,
+ "test": true,
+ "expect": true,
+ "module": true,
+ "asyncTest": true,
+ "start": true,
+ "ok": true,
+ "equal": true,
+ "notEqual": true,
+ "deepEqual": true,
+ "window": true,
+ "document": true,
+ "performance": true
+ }
+}
diff --git a/public/Sortable-1.4.0/CONTRIBUTING.md b/public/Sortable-1.4.0/CONTRIBUTING.md
new file mode 100644
index 00000000..34ff6cdc
--- /dev/null
+++ b/public/Sortable-1.4.0/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+# Contribution Guidelines
+
+### Issue
+
+ 1. Try [dev](https://github.com/RubaXa/Sortable/tree/dev/)-branch, perhaps the problem has been solved;
+ 2. [Use the search](https://github.com/RubaXa/Sortable/search?q=problem), maybe already have an answer;
+ 3. If not found, create example on [jsbin.com (draft)](http://jsbin.com/zunibaxada/1/edit?html,js,output) and describe the problem.
+
+---
+
+### Pull Request
+
+ 1. Before PR run `grunt`;
+ 2. Only into [dev](https://github.com/RubaXa/Sortable/tree/dev/)-branch.
+
+### Setup
+
+ Pieced together from [gruntjs](http://gruntjs.com/getting-started)
+
+ 1. Fork repo on [github](https://github.com)
+ 2. Clone locally
+ 3. from local repro ```npm install```
+ 4. Install grunt-cli globally ```sudo -H npm install -g grunt-cli```
diff --git a/public/Sortable-1.4.0/README.md b/public/Sortable-1.4.0/README.md
new file mode 100644
index 00000000..41d4751c
--- /dev/null
+++ b/public/Sortable-1.4.0/README.md
@@ -0,0 +1,713 @@
+# Sortable
+Sortable is a minimalist JavaScript library for reorderable drag-and-drop lists.
+
+Demo: http://rubaxa.github.io/Sortable/
+
+
+## Features
+
+ * Supports touch devices and [modern](http://caniuse.com/#search=drag) browsers (including IE9)
+ * Can drag from one list to another or within the same list
+ * CSS animation when moving items
+ * Supports drag handles *and selectable text* (better than voidberg's html5sortable)
+ * Smart auto-scrolling
+ * Built using native HTML5 drag and drop API
+ * Supports [Meteor](meteor/README.md), [AngularJS](#ng), [React](#react) and [Polymer](#polymer)
+ * Supports any CSS library, e.g. [Bootstrap](#bs)
+ * Simple API
+ * [CDN](#cdn)
+ * No jQuery (but there is [support](#jq))
+
+
+
+
+
+### Articles
+ * [Sortable v1.0 — New capabilities](https://github.com/RubaXa/Sortable/wiki/Sortable-v1.0-—-New-capabilities/) (December 22, 2014)
+ * [Sorting with the help of HTML5 Drag'n'Drop API](https://github.com/RubaXa/Sortable/wiki/Sorting-with-the-help-of-HTML5-Drag'n'Drop-API/) (December 23, 2013)
+
+
+
+
+
+### Usage
+```html
+
+ item 1
+ item 2
+ item 3
+
+```
+
+```js
+var el = document.getElementById('items');
+var sortable = Sortable.create(el);
+```
+
+You can use any element for the list and its elements, not just `ul`/`li`. Here is an [example with `div`s](http://jsbin.com/luxero/2/edit?html,js,output).
+
+
+---
+
+
+### Options
+```js
+var sortable = new Sortable(el, {
+ group: "name", // or { name: "...", pull: [true, false, clone], put: [true, false, array] }
+ sort: true, // sorting inside list
+ delay: 0, // time in milliseconds to define when the sorting should start
+ disabled: false, // Disables the sortable if set to true.
+ store: null, // @see Store
+ animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
+ handle: ".my-handle", // Drag handle selector within list items
+ filter: ".ignore-elements", // Selectors that do not lead to dragging (String or Function)
+ draggable: ".item", // Specifies which items inside the element should be sortable
+ ghostClass: "sortable-ghost", // Class name for the drop placeholder
+ chosenClass: "sortable-chosen", // Class name for the chosen item
+ dataIdAttr: 'data-id',
+
+ forceFallback: false, // ignore the HTML5 DnD behaviour and force the fallback to kick in
+ fallbackClass: "sortable-fallback" // Class name for the cloned DOM Element when using forceFallback
+ fallbackOnBody: false // Appends the cloned DOM Element into the Document's Body
+
+ scroll: true, // or HTMLElement
+ scrollSensitivity: 30, // px, how near the mouse must be to an edge to start scrolling.
+ scrollSpeed: 10, // px
+
+ setData: function (dataTransfer, dragEl) {
+ dataTransfer.setData('Text', dragEl.textContent);
+ },
+
+ // dragging started
+ onStart: function (/**Event*/evt) {
+ evt.oldIndex; // element index within parent
+ },
+
+ // dragging ended
+ onEnd: function (/**Event*/evt) {
+ evt.oldIndex; // element's old index within parent
+ evt.newIndex; // element's new index within parent
+ },
+
+ // Element is dropped into the list from another list
+ onAdd: function (/**Event*/evt) {
+ var itemEl = evt.item; // dragged HTMLElement
+ evt.from; // previous list
+ // + indexes from onEnd
+ },
+
+ // Changed sorting within list
+ onUpdate: function (/**Event*/evt) {
+ var itemEl = evt.item; // dragged HTMLElement
+ // + indexes from onEnd
+ },
+
+ // Called by any change to the list (add / update / remove)
+ onSort: function (/**Event*/evt) {
+ // same properties as onUpdate
+ },
+
+ // Element is removed from the list into another list
+ onRemove: function (/**Event*/evt) {
+ // same properties as onUpdate
+ },
+
+ // Attempt to drag a filtered element
+ onFilter: function (/**Event*/evt) {
+ var itemEl = evt.item; // HTMLElement receiving the `mousedown|tapstart` event.
+ },
+
+ // Event when you move an item in the list or between lists
+ onMove: function (/**Event*/evt) {
+ // Example: http://jsbin.com/tuyafe/1/edit?js,output
+ evt.dragged; // dragged HTMLElement
+ evt.draggedRect; // TextRectangle {left, top, right и bottom}
+ evt.related; // HTMLElement on which have guided
+ evt.relatedRect; // TextRectangle
+ // return false; — for cancel
+ }
+});
+```
+
+
+---
+
+
+#### `group` option
+To drag elements from one list into another, both lists must have the same `group` value.
+You can also define whether lists can give away, give and keep a copy (`clone`), and receive elements.
+
+ * name: `String` — group name
+ * pull: `true|false|'clone'` — ability to move from the list. `clone` — copy the item, rather than move.
+ * put: `true|false|["foo", "bar"]` — whether elements can be added from other lists, or an array of group names from which elements can be taken. Demo: http://jsbin.com/naduvo/2/edit?html,js,output
+
+
+---
+
+
+#### `sort` option
+Sorting inside list.
+
+Demo: http://jsbin.com/xizeh/2/edit?html,js,output
+
+
+---
+
+
+#### `delay` option
+Time in milliseconds to define when the sorting should start.
+
+Demo: http://jsbin.com/xizeh/4/edit?html,js,output
+
+
+---
+
+
+#### `disabled` options
+Disables the sortable if set to `true`.
+
+Demo: http://jsbin.com/xiloqu/1/edit?html,js,output
+
+```js
+var sortable = Sortable.create(list);
+
+document.getElementById("switcher").onclick = function () {
+ var state = sortable.option("disabled"); // get
+
+ sortable.option("disabled", !state); // set
+};
+```
+
+
+---
+
+
+#### `handle` option
+To make list items draggable, Sortable disables text selection by the user.
+That's not always desirable. To allow text selection, define a drag handler,
+which is an area of every list element that allows it to be dragged around.
+
+Demo: http://jsbin.com/newize/1/edit?html,js,output
+
+```js
+Sortable.create(el, {
+ handle: ".my-handle"
+});
+```
+
+```html
+
+ :: list item text one
+ :: list item text two
+
+```
+
+```css
+.my-handle {
+ cursor: move;
+ cursor: -webkit-grabbing;
+}
+```
+
+
+---
+
+
+#### `filter` option
+
+
+```js
+Sortable.create(list, {
+ filter: ".js-remove, .js-edit",
+ onFilter: function (evt) {
+ var item = evt.item,
+ ctrl = evt.target;
+
+ if (Sortable.utils.is(ctrl, ".js-remove")) { // Click on remove button
+ item.parentNode.removeChild(item); // remove sortable item
+ }
+ else if (Sortable.utils.is(ctrl, ".js-edit")) { // Click on edit link
+ // ...
+ }
+ }
+})
+```
+
+
+---
+
+
+#### `ghostClass` option
+Class name for the drop placeholder (default `sortable-ghost`).
+
+Demo: http://jsbin.com/hunifu/1/edit?css,js,output
+
+```css
+.ghost {
+ opacity: 0.4;
+}
+```
+
+```js
+Sortable.create(list, {
+ ghostClass: "ghost"
+});
+```
+
+
+---
+
+
+#### `chosenClass` option
+Class name for the chosen item (default `sortable-chosen`).
+
+Demo: http://jsbin.com/hunifu/edit?html,css,js,output
+
+```css
+.chosen {
+ color: #fff;
+ background-color: #c00;
+}
+```
+
+```js
+Sortable.create(list, {
+ delay: 500,
+ chosenClass: "chosen"
+});
+```
+
+
+---
+
+
+#### `forceFallback` option
+If set to `true`, the Fallback for non HTML5 Browser will be used, even if we are using an HTML5 Browser.
+This gives us the possiblity to test the behaviour for older Browsers even in newer Browser, or make the Drag 'n Drop feel more consistent between Desktop , Mobile and old Browsers.
+
+On top of that, the Fallback always generates a copy of that DOM Element and appends the class `fallbackClass` definied in the options. This behaviour controls the look of this 'dragged' Element.
+
+Demo: http://jsbin.com/pucurizace/edit?html,css,js,output
+
+
+---
+
+
+#### `scroll` option
+If set to `true`, the page (or sortable-area) scrolls when coming to an edge.
+
+Demo:
+ - `window`: http://jsbin.com/boqugumiqi/1/edit?html,js,output
+ - `overflow: hidden`: http://jsbin.com/kohamakiwi/1/edit?html,js,output
+
+
+---
+
+
+#### `scrollSensitivity` option
+Defines how near the mouse must be to an edge to start scrolling.
+
+
+---
+
+
+#### `scrollSpeed` option
+The speed at which the window should scroll once the mouse pointer gets within the `scrollSensitivity` distance.
+
+
+---
+
+
+
+### Support AngularJS
+Include [ng-sortable.js](ng-sortable.js)
+
+Demo: http://jsbin.com/naduvo/1/edit?html,js,output
+
+```html
+
+```
+
+
+```js
+angular.module('myApp', ['ng-sortable'])
+ .controller('demo', ['$scope', function ($scope) {
+ $scope.items = ['item 1', 'item 2'];
+ $scope.foo = ['foo 1', '..'];
+ $scope.bar = ['bar 1', '..'];
+ $scope.barConfig = {
+ group: 'foobar',
+ animation: 150,
+ onSort: function (/** ngSortEvent */evt){
+ // @see https://github.com/RubaXa/Sortable/blob/master/ng-sortable.js#L18-L24
+ }
+ };
+ }]);
+```
+
+
+---
+
+
+
+### Support React
+Include [react-sortable-mixin.js](react-sortable-mixin.js).
+See [more options](react-sortable-mixin.js#L26).
+
+
+```jsx
+var SortableList = React.createClass({
+ mixins: [SortableMixin],
+
+ getInitialState: function() {
+ return {
+ items: ['Mixin', 'Sortable']
+ };
+ },
+
+ handleSort: function (/** Event */evt) { /*..*/ },
+
+ render: function() {
+ return {
+ this.state.items.map(function (text) {
+ return {text}
+ })
+ }
+ }
+});
+
+React.render( , document.body);
+
+
+//
+// Groups
+//
+var AllUsers = React.createClass({
+ mixins: [SortableMixin],
+
+ sortableOptions: {
+ ref: "user",
+ group: "shared",
+ model: "users"
+ },
+
+ getInitialState: function() {
+ return { users: ['Abbi', 'Adela', 'Bud', 'Cate', 'Davis', 'Eric']; };
+ },
+
+ render: function() {
+ return (
+ Users
+ {
+ this.state.users.map(function (text) {
+ return {text}
+ })
+ }
+ );
+ }
+});
+
+var ApprovedUsers = React.createClass({
+ mixins: [SortableMixin],
+ sortableOptions: { group: "shared" },
+
+ getInitialState: function() {
+ return { items: ['Hal', 'Judy']; };
+ },
+
+ render: function() {
+ return {
+ this.state.items.map(function (text) {
+ return {text}
+ })
+ }
+ }
+});
+
+React.render(, document.body);
+```
+
+
+---
+
+
+
+### Support KnockoutJS
+Include [knockout-sortable.js](knockout-sortable.js)
+
+```html
+
+
+
+
+
+
+
+```
+
+Using this bindingHandler sorts the observableArray when the user sorts the HTMLElements.
+
+The sortable/draggable bindingHandlers supports the same syntax as Knockouts built in [template](http://knockoutjs.com/documentation/template-binding.html) binding except for the `data` option, meaning that you could supply the name of a template or specify a separate templateEngine. The difference between the sortable and draggable handlers is that the draggable has the sortable `group` option set to `{pull:'clone',put: false}` and the `sort` option set to false by default (overridable).
+
+Other attributes are:
+* options: an object that contains settings for the underlaying sortable, ie `group`,`handle`, events etc.
+* collection: if your `foreach` array is a computed then you would supply the underlaying observableArray that you would like to sort here.
+
+
+---
+
+
+### Support Polymer
+```html
+
+
+
+
+
+ {{item}}
+
+
+```
+
+### Method
+
+
+##### option(name:`String`[, value:`*`]):`*`
+Get or set the option.
+
+
+
+##### closest(el:`String`[, selector:`HTMLElement`]):`HTMLElement|null`
+For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+
+
+##### toArray():`String[]`
+Serializes the sortable's item `data-id`'s (`dataIdAttr` option) into an array of string.
+
+
+##### sort(order:`String[]`)
+Sorts the elements according to the array.
+
+```js
+var order = sortable.toArray();
+sortable.sort(order.reverse()); // apply
+```
+
+
+##### save()
+Save the current sorting (see [store](#store))
+
+
+##### destroy()
+Removes the sortable functionality completely.
+
+
+---
+
+
+
+### Store
+Saving and restoring of the sort.
+
+```html
+
+ order
+ save
+ restore
+
+```
+
+```js
+Sortable.create(el, {
+ group: "localStorage-example",
+ store: {
+ /**
+ * Get the order of elements. Called once during initialization.
+ * @param {Sortable} sortable
+ * @returns {Array}
+ */
+ get: function (sortable) {
+ var order = localStorage.getItem(sortable.options.group);
+ return order ? order.split('|') : [];
+ },
+
+ /**
+ * Save the order of elements. Called onEnd (when the item is dropped).
+ * @param {Sortable} sortable
+ */
+ set: function (sortable) {
+ var order = sortable.toArray();
+ localStorage.setItem(sortable.options.group, order.join('|'));
+ }
+ }
+})
+```
+
+
+---
+
+
+
+### Bootstrap
+Demo: http://jsbin.com/luxero/2/edit?html,js,output
+
+```html
+
+
+
+
+
+
+
+
+
+
+ This is Sortable
+ It works with Bootstrap...
+ ...out of the box.
+ It has support for touch devices.
+ Just drag some elements around.
+
+
+
+```
+
+
+---
+
+
+### Static methods & properties
+
+
+
+##### Sortable.create(el:`HTMLElement`[, options:`Object`]):`Sortable`
+Create new instance.
+
+
+---
+
+
+##### Sortable.active:`Sortable`
+Link to the active instance.
+
+
+---
+
+
+##### Sortable.utils
+* on(el`:HTMLElement`, event`:String`, fn`:Function`) — attach an event handler function
+* off(el`:HTMLElement`, event`:String`, fn`:Function`) — remove an event handler
+* css(el`:HTMLElement`)`:Object` — get the values of all the CSS properties
+* css(el`:HTMLElement`, prop`:String`)`:Mixed` — get the value of style properties
+* css(el`:HTMLElement`, prop`:String`, value`:String`) — set one CSS properties
+* css(el`:HTMLElement`, props`:Object`) — set more CSS properties
+* find(ctx`:HTMLElement`, tagName`:String`[, iterator`:Function`])`:Array` — get elements by tag name
+* bind(ctx`:Mixed`, fn`:Function`)`:Function` — Takes a function and returns a new one that will always have a particular context
+* is(el`:HTMLElement`, selector`:String`)`:Boolean` — check the current matched set of elements against a selector
+* closest(el`:HTMLElement`, selector`:String`[, ctx`:HTMLElement`])`:HTMLElement|Null` — for each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree
+* toggleClass(el`:HTMLElement`, name`:String`, state`:Boolean`) — add or remove one classes from each element
+
+
+---
+
+
+
+### CDN
+
+```html
+
+
+
+
+
+
+
+
+
+
+```
+
+
+---
+
+
+
+### jQuery compatibility
+To assemble plugin for jQuery, perform the following steps:
+
+```bash
+ cd Sortable
+ npm install
+ grunt jquery
+```
+
+Now you can use `jquery.fn.sortable.js`:
+(or `jquery.fn.sortable.min.js` if you run `grunt jquery:min`)
+
+```js
+ $("#list").sortable({ /* options */ }); // init
+
+ $("#list").sortable("widget"); // get Sortable instance
+
+ $("#list").sortable("destroy"); // destroy Sortable instance
+
+ $("#list").sortable("{method-name}"); // call an instance method
+
+ $("#list").sortable("{method-name}", "foo", "bar"); // call an instance method with parameters
+```
+
+And `grunt jquery:mySortableFunc` → `jquery.fn.mySortableFunc.js`
+
+---
+
+
+### Contributing (Issue/PR)
+
+Please, [read this](CONTRIBUTING.md).
+
+
+---
+
+
+## MIT LICENSE
+Copyright 2013-2015 Lebedev Konstantin
+http://rubaxa.github.io/Sortable/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/public/Sortable-1.4.0/Sortable.js b/public/Sortable-1.4.0/Sortable.js
new file mode 100644
index 00000000..d767780f
--- /dev/null
+++ b/public/Sortable-1.4.0/Sortable.js
@@ -0,0 +1,1249 @@
+/**!
+ * Sortable
+ * @author RubaXa
+ * @license MIT
+ */
+
+
+(function (factory) {
+ "use strict";
+
+ if (typeof define === "function" && define.amd) {
+ define(factory);
+ }
+ else if (typeof module != "undefined" && typeof module.exports != "undefined") {
+ module.exports = factory();
+ }
+ else if (typeof Package !== "undefined") {
+ Sortable = factory(); // export for Meteor.js
+ }
+ else {
+ /* jshint sub:true */
+ window["Sortable"] = factory();
+ }
+})(function () {
+ "use strict";
+
+ var dragEl,
+ parentEl,
+ ghostEl,
+ cloneEl,
+ rootEl,
+ nextEl,
+
+ scrollEl,
+ scrollParentEl,
+
+ lastEl,
+ lastCSS,
+ lastParentCSS,
+
+ oldIndex,
+ newIndex,
+
+ activeGroup,
+ autoScroll = {},
+
+ tapEvt,
+ touchEvt,
+
+ moved,
+
+ /** @const */
+ RSPACE = /\s+/g,
+
+ expando = 'Sortable' + (new Date).getTime(),
+
+ win = window,
+ document = win.document,
+ parseInt = win.parseInt,
+
+ supportDraggable = !!('draggable' in document.createElement('div')),
+ supportCssPointerEvents = (function (el) {
+ el = document.createElement('x');
+ el.style.cssText = 'pointer-events:auto';
+ return el.style.pointerEvents === 'auto';
+ })(),
+
+ _silent = false,
+
+ abs = Math.abs,
+ slice = [].slice,
+
+ touchDragOverListeners = [],
+
+ _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
+ // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
+ if (rootEl && options.scroll) {
+ var el,
+ rect,
+ sens = options.scrollSensitivity,
+ speed = options.scrollSpeed,
+
+ x = evt.clientX,
+ y = evt.clientY,
+
+ winWidth = window.innerWidth,
+ winHeight = window.innerHeight,
+
+ vx,
+ vy
+ ;
+
+ // Delect scrollEl
+ if (scrollParentEl !== rootEl) {
+ scrollEl = options.scroll;
+ scrollParentEl = rootEl;
+
+ if (scrollEl === true) {
+ scrollEl = rootEl;
+
+ do {
+ if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
+ (scrollEl.offsetHeight < scrollEl.scrollHeight)
+ ) {
+ break;
+ }
+ /* jshint boss:true */
+ } while (scrollEl = scrollEl.parentNode);
+ }
+ }
+
+ if (scrollEl) {
+ el = scrollEl;
+ rect = scrollEl.getBoundingClientRect();
+ vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
+ vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
+ }
+
+
+ if (!(vx || vy)) {
+ vx = (winWidth - x <= sens) - (x <= sens);
+ vy = (winHeight - y <= sens) - (y <= sens);
+
+ /* jshint expr:true */
+ (vx || vy) && (el = win);
+ }
+
+
+ if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
+ autoScroll.el = el;
+ autoScroll.vx = vx;
+ autoScroll.vy = vy;
+
+ clearInterval(autoScroll.pid);
+
+ if (el) {
+ autoScroll.pid = setInterval(function () {
+ if (el === win) {
+ win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed);
+ } else {
+ vy && (el.scrollTop += vy * speed);
+ vx && (el.scrollLeft += vx * speed);
+ }
+ }, 24);
+ }
+ }
+ }
+ }, 30),
+
+ _prepareGroup = function (options) {
+ var group = options.group;
+
+ if (!group || typeof group != 'object') {
+ group = options.group = {name: group};
+ }
+
+ ['pull', 'put'].forEach(function (key) {
+ if (!(key in group)) {
+ group[key] = true;
+ }
+ });
+
+ options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' ';
+ }
+ ;
+
+
+
+ /**
+ * @class Sortable
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ function Sortable(el, options) {
+ if (!(el && el.nodeType && el.nodeType === 1)) {
+ throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el);
+ }
+
+ this.el = el; // root element
+ this.options = options = _extend({}, options);
+
+
+ // Export instance
+ el[expando] = this;
+
+
+ // Default options
+ var defaults = {
+ group: Math.random(),
+ sort: true,
+ disabled: false,
+ store: null,
+ handle: null,
+ scroll: true,
+ scrollSensitivity: 30,
+ scrollSpeed: 10,
+ draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ ignore: 'a, img',
+ filter: null,
+ animation: 0,
+ setData: function (dataTransfer, dragEl) {
+ dataTransfer.setData('Text', dragEl.textContent);
+ },
+ dropBubble: false,
+ dragoverBubble: false,
+ dataIdAttr: 'data-id',
+ delay: 0,
+ forceFallback: false,
+ fallbackClass: 'sortable-fallback',
+ fallbackOnBody: false
+ };
+
+
+ // Set default options
+ for (var name in defaults) {
+ !(name in options) && (options[name] = defaults[name]);
+ }
+
+ _prepareGroup(options);
+
+ // Bind all private methods
+ for (var fn in this) {
+ if (fn.charAt(0) === '_') {
+ this[fn] = this[fn].bind(this);
+ }
+ }
+
+ // Setup drag mode
+ this.nativeDraggable = options.forceFallback ? false : supportDraggable;
+
+ // Bind events
+ _on(el, 'mousedown', this._onTapStart);
+ _on(el, 'touchstart', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ _on(el, 'dragover', this);
+ _on(el, 'dragenter', this);
+ }
+
+ touchDragOverListeners.push(this._onDragOver);
+
+ // Restore sorting
+ options.store && this.sort(options.store.get(this));
+ }
+
+
+ Sortable.prototype = /** @lends Sortable.prototype */ {
+ constructor: Sortable,
+
+ _onTapStart: function (/** Event|TouchEvent */evt) {
+ var _this = this,
+ el = this.el,
+ options = this.options,
+ type = evt.type,
+ touch = evt.touches && evt.touches[0],
+ target = (touch || evt).target,
+ originalTarget = target,
+ filter = options.filter;
+
+
+ if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
+ return; // only left button or enabled
+ }
+
+ target = _closest(target, options.draggable, el);
+
+ if (!target) {
+ return;
+ }
+
+ // get the index of the dragged element within its parent
+ oldIndex = _index(target);
+
+ // Check filter
+ if (typeof filter === 'function') {
+ if (filter.call(this, evt, target, this)) {
+ _dispatchEvent(_this, originalTarget, 'filter', target, el, oldIndex);
+ evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+ else if (filter) {
+ filter = filter.split(',').some(function (criteria) {
+ criteria = _closest(originalTarget, criteria.trim(), el);
+
+ if (criteria) {
+ _dispatchEvent(_this, criteria, 'filter', target, el, oldIndex);
+ return true;
+ }
+ });
+
+ if (filter) {
+ evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+
+
+ if (options.handle && !_closest(originalTarget, options.handle, el)) {
+ return;
+ }
+
+
+ // Prepare `dragstart`
+ this._prepareDragStart(evt, touch, target);
+ },
+
+ _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target) {
+ var _this = this,
+ el = _this.el,
+ options = _this.options,
+ ownerDocument = el.ownerDocument,
+ dragStartFn;
+
+ if (target && !dragEl && (target.parentNode === el)) {
+ tapEvt = evt;
+
+ rootEl = el;
+ dragEl = target;
+ parentEl = dragEl.parentNode;
+ nextEl = dragEl.nextSibling;
+ activeGroup = options.group;
+
+ dragStartFn = function () {
+ // Delayed drag has been triggered
+ // we can re-enable the events: touchmove/mousemove
+ _this._disableDelayedDrag();
+
+ // Make the element draggable
+ dragEl.draggable = true;
+
+ // Chosen item
+ _toggleClass(dragEl, _this.options.chosenClass, true);
+
+ // Bind the events: dragstart/dragend
+ _this._triggerDragStart(touch);
+ };
+
+ // Disable "draggable"
+ options.ignore.split(',').forEach(function (criteria) {
+ _find(dragEl, criteria.trim(), _disableDraggable);
+ });
+
+ _on(ownerDocument, 'mouseup', _this._onDrop);
+ _on(ownerDocument, 'touchend', _this._onDrop);
+ _on(ownerDocument, 'touchcancel', _this._onDrop);
+
+ if (options.delay) {
+ // If the user moves the pointer or let go the click or touch
+ // before the delay has been reached:
+ // disable the delayed drag
+ _on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchend', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
+ _on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchmove', _this._disableDelayedDrag);
+
+ _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
+ } else {
+ dragStartFn();
+ }
+ }
+ },
+
+ _disableDelayedDrag: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ clearTimeout(this._dragStartTimer);
+ _off(ownerDocument, 'mouseup', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchend', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
+ _off(ownerDocument, 'mousemove', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchmove', this._disableDelayedDrag);
+ },
+
+ _triggerDragStart: function (/** Touch */touch) {
+ if (touch) {
+ // Touch device support
+ tapEvt = {
+ target: dragEl,
+ clientX: touch.clientX,
+ clientY: touch.clientY
+ };
+
+ this._onDragStart(tapEvt, 'touch');
+ }
+ else if (!this.nativeDraggable) {
+ this._onDragStart(tapEvt, true);
+ }
+ else {
+ _on(dragEl, 'dragend', this);
+ _on(rootEl, 'dragstart', this._onDragStart);
+ }
+
+ try {
+ if (document.selection) {
+ document.selection.empty();
+ } else {
+ window.getSelection().removeAllRanges();
+ }
+ } catch (err) {
+ }
+ },
+
+ _dragStarted: function () {
+ if (rootEl && dragEl) {
+ // Apply effect
+ _toggleClass(dragEl, this.options.ghostClass, true);
+
+ Sortable.active = this;
+
+ // Drag start event
+ _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex);
+ }
+ },
+
+ _emulateDragOver: function () {
+ if (touchEvt) {
+ if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) {
+ return;
+ }
+
+ this._lastX = touchEvt.clientX;
+ this._lastY = touchEvt.clientY;
+
+ if (!supportCssPointerEvents) {
+ _css(ghostEl, 'display', 'none');
+ }
+
+ var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
+ parent = target,
+ groupName = ' ' + this.options.group.name + '',
+ i = touchDragOverListeners.length;
+
+ if (parent) {
+ do {
+ if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) {
+ while (i--) {
+ touchDragOverListeners[i]({
+ clientX: touchEvt.clientX,
+ clientY: touchEvt.clientY,
+ target: target,
+ rootEl: parent
+ });
+ }
+
+ break;
+ }
+
+ target = parent; // store last element
+ }
+ /* jshint boss:true */
+ while (parent = parent.parentNode);
+ }
+
+ if (!supportCssPointerEvents) {
+ _css(ghostEl, 'display', '');
+ }
+ }
+ },
+
+
+ _onTouchMove: function (/**TouchEvent*/evt) {
+ if (tapEvt) {
+ // only set the status to dragging, when we are actually dragging
+ if (!Sortable.active) {
+ this._dragStarted();
+ }
+
+ // as well as creating the ghost element on the document body
+ this._appendGhost();
+
+ var touch = evt.touches ? evt.touches[0] : evt,
+ dx = touch.clientX - tapEvt.clientX,
+ dy = touch.clientY - tapEvt.clientY,
+ translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
+
+ moved = true;
+ touchEvt = touch;
+
+ _css(ghostEl, 'webkitTransform', translate3d);
+ _css(ghostEl, 'mozTransform', translate3d);
+ _css(ghostEl, 'msTransform', translate3d);
+ _css(ghostEl, 'transform', translate3d);
+
+ evt.preventDefault();
+ }
+ },
+
+ _appendGhost: function () {
+ if (!ghostEl) {
+ var rect = dragEl.getBoundingClientRect(),
+ css = _css(dragEl),
+ options = this.options,
+ ghostRect;
+
+ ghostEl = dragEl.cloneNode(true);
+
+ _toggleClass(ghostEl, options.ghostClass, false);
+ _toggleClass(ghostEl, options.fallbackClass, true);
+
+ _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
+ _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
+ _css(ghostEl, 'width', rect.width);
+ _css(ghostEl, 'height', rect.height);
+ _css(ghostEl, 'opacity', '0.8');
+ _css(ghostEl, 'position', 'fixed');
+ _css(ghostEl, 'zIndex', '100000');
+ _css(ghostEl, 'pointerEvents', 'none');
+
+ options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);
+
+ // Fixing dimensions.
+ ghostRect = ghostEl.getBoundingClientRect();
+ _css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
+ _css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
+ }
+ },
+
+ _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
+ var dataTransfer = evt.dataTransfer,
+ options = this.options;
+
+ this._offUpEvents();
+
+ if (activeGroup.pull == 'clone') {
+ cloneEl = dragEl.cloneNode(true);
+ _css(cloneEl, 'display', 'none');
+ rootEl.insertBefore(cloneEl, dragEl);
+ }
+
+ if (useFallback) {
+
+ if (useFallback === 'touch') {
+ // Bind touch events
+ _on(document, 'touchmove', this._onTouchMove);
+ _on(document, 'touchend', this._onDrop);
+ _on(document, 'touchcancel', this._onDrop);
+ } else {
+ // Old brwoser
+ _on(document, 'mousemove', this._onTouchMove);
+ _on(document, 'mouseup', this._onDrop);
+ }
+
+ this._loopId = setInterval(this._emulateDragOver, 50);
+ }
+ else {
+ if (dataTransfer) {
+ dataTransfer.effectAllowed = 'move';
+ options.setData && options.setData.call(this, dataTransfer, dragEl);
+ }
+
+ _on(document, 'drop', this);
+ setTimeout(this._dragStarted, 0);
+ }
+ },
+
+ _onDragOver: function (/**Event*/evt) {
+ var el = this.el,
+ target,
+ dragRect,
+ revert,
+ options = this.options,
+ group = options.group,
+ groupPut = group.put,
+ isOwner = (activeGroup === group),
+ canSort = options.sort;
+
+ if (evt.preventDefault !== void 0) {
+ evt.preventDefault();
+ !options.dragoverBubble && evt.stopPropagation();
+ }
+
+ moved = true;
+
+ if (activeGroup && !options.disabled &&
+ (isOwner
+ ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
+ : activeGroup.pull && groupPut && (
+ (activeGroup.name === group.name) || // by Name
+ (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array
+ )
+ ) &&
+ (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
+ ) {
+ // Smart auto-scrolling
+ _autoScroll(evt, options, this.el);
+
+ if (_silent) {
+ return;
+ }
+
+ target = _closest(evt.target, options.draggable, el);
+ dragRect = dragEl.getBoundingClientRect();
+
+ if (revert) {
+ _cloneHide(true);
+
+ if (cloneEl || nextEl) {
+ rootEl.insertBefore(dragEl, cloneEl || nextEl);
+ }
+ else if (!canSort) {
+ rootEl.appendChild(dragEl);
+ }
+
+ return;
+ }
+
+
+ if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
+ (el === evt.target) && (target = _ghostIsLast(el, evt))
+ ) {
+
+ if (target) {
+ if (target.animated) {
+ return;
+ }
+
+ targetRect = target.getBoundingClientRect();
+ }
+
+ _cloneHide(isOwner);
+
+ if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) {
+ if (!dragEl.contains(el)) {
+ el.appendChild(dragEl);
+ parentEl = el; // actualization
+ }
+
+ this._animate(dragRect, dragEl);
+ target && this._animate(targetRect, target);
+ }
+ }
+ else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
+ if (lastEl !== target) {
+ lastEl = target;
+ lastCSS = _css(target);
+ lastParentCSS = _css(target.parentNode);
+ }
+
+
+ var targetRect = target.getBoundingClientRect(),
+ width = targetRect.right - targetRect.left,
+ height = targetRect.bottom - targetRect.top,
+ floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
+ || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
+ isWide = (target.offsetWidth > dragEl.offsetWidth),
+ isLong = (target.offsetHeight > dragEl.offsetHeight),
+ halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
+ nextSibling = target.nextElementSibling,
+ moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect),
+ after
+ ;
+
+ if (moveVector !== false) {
+ _silent = true;
+ setTimeout(_unsilent, 30);
+
+ _cloneHide(isOwner);
+
+ if (moveVector === 1 || moveVector === -1) {
+ after = (moveVector === 1);
+ }
+ else if (floating) {
+ var elTop = dragEl.offsetTop,
+ tgTop = target.offsetTop;
+
+ if (elTop === tgTop) {
+ after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
+ } else {
+ after = tgTop > elTop;
+ }
+ } else {
+ after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
+ }
+
+ if (!dragEl.contains(el)) {
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
+ } else {
+ target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+ }
+ }
+
+ parentEl = dragEl.parentNode; // actualization
+
+ this._animate(dragRect, dragEl);
+ this._animate(targetRect, target);
+ }
+ }
+ }
+ },
+
+ _animate: function (prevRect, target) {
+ var ms = this.options.animation;
+
+ if (ms) {
+ var currentRect = target.getBoundingClientRect();
+
+ _css(target, 'transition', 'none');
+ _css(target, 'transform', 'translate3d('
+ + (prevRect.left - currentRect.left) + 'px,'
+ + (prevRect.top - currentRect.top) + 'px,0)'
+ );
+
+ target.offsetWidth; // repaint
+
+ _css(target, 'transition', 'all ' + ms + 'ms');
+ _css(target, 'transform', 'translate3d(0,0,0)');
+
+ clearTimeout(target.animated);
+ target.animated = setTimeout(function () {
+ _css(target, 'transition', '');
+ _css(target, 'transform', '');
+ target.animated = false;
+ }, ms);
+ }
+ },
+
+ _offUpEvents: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ _off(document, 'touchmove', this._onTouchMove);
+ _off(ownerDocument, 'mouseup', this._onDrop);
+ _off(ownerDocument, 'touchend', this._onDrop);
+ _off(ownerDocument, 'touchcancel', this._onDrop);
+ },
+
+ _onDrop: function (/**Event*/evt) {
+ var el = this.el,
+ options = this.options;
+
+ clearInterval(this._loopId);
+ clearInterval(autoScroll.pid);
+ clearTimeout(this._dragStartTimer);
+
+ // Unbind events
+ _off(document, 'mousemove', this._onTouchMove);
+
+ if (this.nativeDraggable) {
+ _off(document, 'drop', this);
+ _off(el, 'dragstart', this._onDragStart);
+ }
+
+ this._offUpEvents();
+
+ if (evt) {
+ if (moved) {
+ evt.preventDefault();
+ !options.dropBubble && evt.stopPropagation();
+ }
+
+ ghostEl && ghostEl.parentNode.removeChild(ghostEl);
+
+ if (dragEl) {
+ if (this.nativeDraggable) {
+ _off(dragEl, 'dragend', this);
+ }
+
+ _disableDraggable(dragEl);
+
+ // Remove class's
+ _toggleClass(dragEl, this.options.ghostClass, false);
+ _toggleClass(dragEl, this.options.chosenClass, false);
+
+ if (rootEl !== parentEl) {
+ newIndex = _index(dragEl);
+
+ if (newIndex >= 0) {
+ // drag from one list and drop into another
+ _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+
+ // Add event
+ _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);
+
+ // Remove event
+ _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
+ }
+ }
+ else {
+ // Remove clone
+ cloneEl && cloneEl.parentNode.removeChild(cloneEl);
+
+ if (dragEl.nextSibling !== nextEl) {
+ // Get the index of the dragged element within its parent
+ newIndex = _index(dragEl);
+
+ if (newIndex >= 0) {
+ // drag & drop within the same list
+ _dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+ }
+ }
+ }
+
+ if (Sortable.active) {
+ if (newIndex === null || newIndex === -1) {
+ newIndex = oldIndex;
+ }
+
+ _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);
+
+ // Save sorting
+ this.save();
+ }
+ }
+
+ // Nulling
+ rootEl =
+ dragEl =
+ parentEl =
+ ghostEl =
+ nextEl =
+ cloneEl =
+
+ scrollEl =
+ scrollParentEl =
+
+ tapEvt =
+ touchEvt =
+
+ moved =
+ newIndex =
+
+ lastEl =
+ lastCSS =
+
+ activeGroup =
+ Sortable.active = null;
+ }
+ },
+
+
+ handleEvent: function (/**Event*/evt) {
+ var type = evt.type;
+
+ if (type === 'dragover' || type === 'dragenter') {
+ if (dragEl) {
+ this._onDragOver(evt);
+ _globalDragOver(evt);
+ }
+ }
+ else if (type === 'drop' || type === 'dragend') {
+ this._onDrop(evt);
+ }
+ },
+
+
+ /**
+ * Serializes the item into an array of string.
+ * @returns {String[]}
+ */
+ toArray: function () {
+ var order = [],
+ el,
+ children = this.el.children,
+ i = 0,
+ n = children.length,
+ options = this.options;
+
+ for (; i < n; i++) {
+ el = children[i];
+ if (_closest(el, options.draggable, this.el)) {
+ order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
+ }
+ }
+
+ return order;
+ },
+
+
+ /**
+ * Sorts the elements according to the array.
+ * @param {String[]} order order of the items
+ */
+ sort: function (order) {
+ var items = {}, rootEl = this.el;
+
+ this.toArray().forEach(function (id, i) {
+ var el = rootEl.children[i];
+
+ if (_closest(el, this.options.draggable, rootEl)) {
+ items[id] = el;
+ }
+ }, this);
+
+ order.forEach(function (id) {
+ if (items[id]) {
+ rootEl.removeChild(items[id]);
+ rootEl.appendChild(items[id]);
+ }
+ });
+ },
+
+
+ /**
+ * Save the current sorting
+ */
+ save: function () {
+ var store = this.options.store;
+ store && store.set(this);
+ },
+
+
+ /**
+ * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+ * @param {HTMLElement} el
+ * @param {String} [selector] default: `options.draggable`
+ * @returns {HTMLElement|null}
+ */
+ closest: function (el, selector) {
+ return _closest(el, selector || this.options.draggable, this.el);
+ },
+
+
+ /**
+ * Set/get option
+ * @param {string} name
+ * @param {*} [value]
+ * @returns {*}
+ */
+ option: function (name, value) {
+ var options = this.options;
+
+ if (value === void 0) {
+ return options[name];
+ } else {
+ options[name] = value;
+
+ if (name === 'group') {
+ _prepareGroup(options);
+ }
+ }
+ },
+
+
+ /**
+ * Destroy
+ */
+ destroy: function () {
+ var el = this.el;
+
+ el[expando] = null;
+
+ _off(el, 'mousedown', this._onTapStart);
+ _off(el, 'touchstart', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ _off(el, 'dragover', this);
+ _off(el, 'dragenter', this);
+ }
+
+ // Remove draggable attributes
+ Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
+ el.removeAttribute('draggable');
+ });
+
+ touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
+
+ this._onDrop();
+
+ this.el = el = null;
+ }
+ };
+
+
+ function _cloneHide(state) {
+ if (cloneEl && (cloneEl.state !== state)) {
+ _css(cloneEl, 'display', state ? 'none' : '');
+ !state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
+ cloneEl.state = state;
+ }
+ }
+
+
+ function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
+ if (el) {
+ ctx = ctx || document;
+ selector = selector.split('.');
+
+ var tag = selector.shift().toUpperCase(),
+ re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g');
+
+ do {
+ if (
+ (tag === '>*' && el.parentNode === ctx) || (
+ (tag === '' || el.nodeName.toUpperCase() == tag) &&
+ (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
+ )
+ ) {
+ return el;
+ }
+ }
+ while (el !== ctx && (el = el.parentNode));
+ }
+
+ return null;
+ }
+
+
+ function _globalDragOver(/**Event*/evt) {
+ if (evt.dataTransfer) {
+ evt.dataTransfer.dropEffect = 'move';
+ }
+ evt.preventDefault();
+ }
+
+
+ function _on(el, event, fn) {
+ el.addEventListener(event, fn, false);
+ }
+
+
+ function _off(el, event, fn) {
+ el.removeEventListener(event, fn, false);
+ }
+
+
+ function _toggleClass(el, name, state) {
+ if (el) {
+ if (el.classList) {
+ el.classList[state ? 'add' : 'remove'](name);
+ }
+ else {
+ var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' ');
+ el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' ');
+ }
+ }
+ }
+
+
+ function _css(el, prop, val) {
+ var style = el && el.style;
+
+ if (style) {
+ if (val === void 0) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ val = document.defaultView.getComputedStyle(el, '');
+ }
+ else if (el.currentStyle) {
+ val = el.currentStyle;
+ }
+
+ return prop === void 0 ? val : val[prop];
+ }
+ else {
+ if (!(prop in style)) {
+ prop = '-webkit-' + prop;
+ }
+
+ style[prop] = val + (typeof val === 'string' ? '' : 'px');
+ }
+ }
+ }
+
+
+ function _find(ctx, tagName, iterator) {
+ if (ctx) {
+ var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
+
+ if (iterator) {
+ for (; i < n; i++) {
+ iterator(list[i], i);
+ }
+ }
+
+ return list;
+ }
+
+ return [];
+ }
+
+
+
+ function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
+ var evt = document.createEvent('Event'),
+ options = (sortable || rootEl[expando]).options,
+ onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
+
+ evt.initEvent(name, true, true);
+
+ evt.to = rootEl;
+ evt.from = fromEl || rootEl;
+ evt.item = targetEl || rootEl;
+ evt.clone = cloneEl;
+
+ evt.oldIndex = startIndex;
+ evt.newIndex = newIndex;
+
+ rootEl.dispatchEvent(evt);
+
+ if (options[onName]) {
+ options[onName].call(sortable, evt);
+ }
+ }
+
+
+ function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) {
+ var evt,
+ sortable = fromEl[expando],
+ onMoveFn = sortable.options.onMove,
+ retVal;
+
+ evt = document.createEvent('Event');
+ evt.initEvent('move', true, true);
+
+ evt.to = toEl;
+ evt.from = fromEl;
+ evt.dragged = dragEl;
+ evt.draggedRect = dragRect;
+ evt.related = targetEl || toEl;
+ evt.relatedRect = targetRect || toEl.getBoundingClientRect();
+
+ fromEl.dispatchEvent(evt);
+
+ if (onMoveFn) {
+ retVal = onMoveFn.call(sortable, evt);
+ }
+
+ return retVal;
+ }
+
+
+ function _disableDraggable(el) {
+ el.draggable = false;
+ }
+
+
+ function _unsilent() {
+ _silent = false;
+ }
+
+
+ /** @returns {HTMLElement|false} */
+ function _ghostIsLast(el, evt) {
+ var lastEl = el.lastElementChild,
+ rect = lastEl.getBoundingClientRect();
+
+ return ((evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.right + rect.width) > 5)) && lastEl; // min delta
+ }
+
+
+ /**
+ * Generate id
+ * @param {HTMLElement} el
+ * @returns {String}
+ * @private
+ */
+ function _generateId(el) {
+ var str = el.tagName + el.className + el.src + el.href + el.textContent,
+ i = str.length,
+ sum = 0;
+
+ while (i--) {
+ sum += str.charCodeAt(i);
+ }
+
+ return sum.toString(36);
+ }
+
+ /**
+ * Returns the index of an element within its parent
+ * @param {HTMLElement} el
+ * @return {number}
+ */
+ function _index(el) {
+ var index = 0;
+
+ if (!el || !el.parentNode) {
+ return -1;
+ }
+
+ while (el && (el = el.previousElementSibling)) {
+ if (el.nodeName.toUpperCase() !== 'TEMPLATE') {
+ index++;
+ }
+ }
+
+ return index;
+ }
+
+ function _throttle(callback, ms) {
+ var args, _this;
+
+ return function () {
+ if (args === void 0) {
+ args = arguments;
+ _this = this;
+
+ setTimeout(function () {
+ if (args.length === 1) {
+ callback.call(_this, args[0]);
+ } else {
+ callback.apply(_this, args);
+ }
+
+ args = void 0;
+ }, ms);
+ }
+ };
+ }
+
+ function _extend(dst, src) {
+ if (dst && src) {
+ for (var key in src) {
+ if (src.hasOwnProperty(key)) {
+ dst[key] = src[key];
+ }
+ }
+ }
+
+ return dst;
+ }
+
+
+ // Export utils
+ Sortable.utils = {
+ on: _on,
+ off: _off,
+ css: _css,
+ find: _find,
+ is: function (el, selector) {
+ return !!_closest(el, selector, el);
+ },
+ extend: _extend,
+ throttle: _throttle,
+ closest: _closest,
+ toggleClass: _toggleClass,
+ index: _index
+ };
+
+
+ /**
+ * Create sortable instance
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ Sortable.create = function (el, options) {
+ return new Sortable(el, options);
+ };
+
+
+ // Export
+ Sortable.version = '1.3.0';
+ return Sortable;
+});
diff --git a/public/Sortable-1.4.0/Sortable.min.js b/public/Sortable-1.4.0/Sortable.min.js
new file mode 100644
index 00000000..bef7fa3a
--- /dev/null
+++ b/public/Sortable-1.4.0/Sortable.min.js
@@ -0,0 +1,2 @@
+/*! Sortable 1.3.0 - MIT | git://github.com/rubaxa/Sortable.git */
+!function(a){"use strict";"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=a():"undefined"!=typeof Package?Sortable=a():window.Sortable=a()}(function(){"use strict";function a(a,b){if(!a||!a.nodeType||1!==a.nodeType)throw"Sortable: `el` must be HTMLElement, and not "+{}.toString.call(a);this.el=a,this.options=b=r({},b),a[L]=this;var c={group:Math.random(),sort:!0,disabled:!1,store:null,handle:null,scroll:!0,scrollSensitivity:30,scrollSpeed:10,draggable:/[uo]l/i.test(a.nodeName)?"li":">*",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",ignore:"a, img",filter:null,animation:0,setData:function(a,b){a.setData("Text",b.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1};for(var d in c)!(d in b)&&(b[d]=c[d]);V(b);for(var f in this)"_"===f.charAt(0)&&(this[f]=this[f].bind(this));this.nativeDraggable=b.forceFallback?!1:P,e(a,"mousedown",this._onTapStart),e(a,"touchstart",this._onTapStart),this.nativeDraggable&&(e(a,"dragover",this),e(a,"dragenter",this)),T.push(this._onDragOver),b.store&&this.sort(b.store.get(this))}function b(a){v&&v.state!==a&&(h(v,"display",a?"none":""),!a&&v.state&&w.insertBefore(v,s),v.state=a)}function c(a,b,c){if(a){c=c||N,b=b.split(".");var d=b.shift().toUpperCase(),e=new RegExp("\\s("+b.join("|")+")(?=\\s)","g");do if(">*"===d&&a.parentNode===c||(""===d||a.nodeName.toUpperCase()==d)&&(!b.length||((" "+a.className+" ").match(e)||[]).length==b.length))return a;while(a!==c&&(a=a.parentNode))}return null}function d(a){a.dataTransfer&&(a.dataTransfer.dropEffect="move"),a.preventDefault()}function e(a,b,c){a.addEventListener(b,c,!1)}function f(a,b,c){a.removeEventListener(b,c,!1)}function g(a,b,c){if(a)if(a.classList)a.classList[c?"add":"remove"](b);else{var d=(" "+a.className+" ").replace(K," ").replace(" "+b+" "," ");a.className=(d+(c?" "+b:"")).replace(K," ")}}function h(a,b,c){var d=a&&a.style;if(d){if(void 0===c)return N.defaultView&&N.defaultView.getComputedStyle?c=N.defaultView.getComputedStyle(a,""):a.currentStyle&&(c=a.currentStyle),void 0===b?c:c[b];b in d||(b="-webkit-"+b),d[b]=c+("string"==typeof c?"":"px")}}function i(a,b,c){if(a){var d=a.getElementsByTagName(b),e=0,f=d.length;if(c)for(;f>e;e++)c(d[e],e);return d}return[]}function j(a,b,c,d,e,f,g){var h=N.createEvent("Event"),i=(a||b[L]).options,j="on"+c.charAt(0).toUpperCase()+c.substr(1);h.initEvent(c,!0,!0),h.to=b,h.from=e||b,h.item=d||b,h.clone=v,h.oldIndex=f,h.newIndex=g,b.dispatchEvent(h),i[j]&&i[j].call(a,h)}function k(a,b,c,d,e,f){var g,h,i=a[L],j=i.options.onMove;return g=N.createEvent("Event"),g.initEvent("move",!0,!0),g.to=b,g.from=a,g.dragged=c,g.draggedRect=d,g.related=e||b,g.relatedRect=f||b.getBoundingClientRect(),a.dispatchEvent(g),j&&(h=j.call(i,g)),h}function l(a){a.draggable=!1}function m(){R=!1}function n(a,b){var c=a.lastElementChild,d=c.getBoundingClientRect();return(b.clientY-(d.top+d.height)>5||b.clientX-(d.right+d.width)>5)&&c}function o(a){for(var b=a.tagName+a.className+a.src+a.href+a.textContent,c=b.length,d=0;c--;)d+=b.charCodeAt(c);return d.toString(36)}function p(a){var b=0;if(!a||!a.parentNode)return-1;for(;a&&(a=a.previousElementSibling);)"TEMPLATE"!==a.nodeName.toUpperCase()&&b++;return b}function q(a,b){var c,d;return function(){void 0===c&&(c=arguments,d=this,setTimeout(function(){1===c.length?a.call(d,c[0]):a.apply(d,c),c=void 0},b))}}function r(a,b){if(a&&b)for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a}var s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J={},K=/\s+/g,L="Sortable"+(new Date).getTime(),M=window,N=M.document,O=M.parseInt,P=!!("draggable"in N.createElement("div")),Q=function(a){return a=N.createElement("x"),a.style.cssText="pointer-events:auto","auto"===a.style.pointerEvents}(),R=!1,S=Math.abs,T=([].slice,[]),U=q(function(a,b,c){if(c&&b.scroll){var d,e,f,g,h=b.scrollSensitivity,i=b.scrollSpeed,j=a.clientX,k=a.clientY,l=window.innerWidth,m=window.innerHeight;if(z!==c&&(y=b.scroll,z=c,y===!0)){y=c;do if(y.offsetWidth=l-j)-(h>=j),g=(h>=m-k)-(h>=k),(f||g)&&(d=M)),(J.vx!==f||J.vy!==g||J.el!==d)&&(J.el=d,J.vx=f,J.vy=g,clearInterval(J.pid),d&&(J.pid=setInterval(function(){d===M?M.scrollTo(M.pageXOffset+f*i,M.pageYOffset+g*i):(g&&(d.scrollTop+=g*i),f&&(d.scrollLeft+=f*i))},24)))}},30),V=function(a){var b=a.group;b&&"object"==typeof b||(b=a.group={name:b}),["pull","put"].forEach(function(a){a in b||(b[a]=!0)}),a.groups=" "+b.name+(b.put.join?" "+b.put.join(" "):"")+" "};return a.prototype={constructor:a,_onTapStart:function(a){var b=this,d=this.el,e=this.options,f=a.type,g=a.touches&&a.touches[0],h=(g||a).target,i=h,k=e.filter;if(!("mousedown"===f&&0!==a.button||e.disabled)&&(h=c(h,e.draggable,d))){if(D=p(h),"function"==typeof k){if(k.call(this,a,h,this))return j(b,i,"filter",h,d,D),void a.preventDefault()}else if(k&&(k=k.split(",").some(function(a){return a=c(i,a.trim(),d),a?(j(b,a,"filter",h,d,D),!0):void 0})))return void a.preventDefault();(!e.handle||c(i,e.handle,d))&&this._prepareDragStart(a,g,h)}},_prepareDragStart:function(a,b,c){var d,f=this,h=f.el,j=f.options,k=h.ownerDocument;c&&!s&&c.parentNode===h&&(G=a,w=h,s=c,t=s.parentNode,x=s.nextSibling,F=j.group,d=function(){f._disableDelayedDrag(),s.draggable=!0,g(s,f.options.chosenClass,!0),f._triggerDragStart(b)},j.ignore.split(",").forEach(function(a){i(s,a.trim(),l)}),e(k,"mouseup",f._onDrop),e(k,"touchend",f._onDrop),e(k,"touchcancel",f._onDrop),j.delay?(e(k,"mouseup",f._disableDelayedDrag),e(k,"touchend",f._disableDelayedDrag),e(k,"touchcancel",f._disableDelayedDrag),e(k,"mousemove",f._disableDelayedDrag),e(k,"touchmove",f._disableDelayedDrag),f._dragStartTimer=setTimeout(d,j.delay)):d())},_disableDelayedDrag:function(){var a=this.el.ownerDocument;clearTimeout(this._dragStartTimer),f(a,"mouseup",this._disableDelayedDrag),f(a,"touchend",this._disableDelayedDrag),f(a,"touchcancel",this._disableDelayedDrag),f(a,"mousemove",this._disableDelayedDrag),f(a,"touchmove",this._disableDelayedDrag)},_triggerDragStart:function(a){a?(G={target:s,clientX:a.clientX,clientY:a.clientY},this._onDragStart(G,"touch")):this.nativeDraggable?(e(s,"dragend",this),e(w,"dragstart",this._onDragStart)):this._onDragStart(G,!0);try{N.selection?N.selection.empty():window.getSelection().removeAllRanges()}catch(b){}},_dragStarted:function(){w&&s&&(g(s,this.options.ghostClass,!0),a.active=this,j(this,w,"start",s,w,D))},_emulateDragOver:function(){if(H){if(this._lastX===H.clientX&&this._lastY===H.clientY)return;this._lastX=H.clientX,this._lastY=H.clientY,Q||h(u,"display","none");var a=N.elementFromPoint(H.clientX,H.clientY),b=a,c=" "+this.options.group.name,d=T.length;if(b)do{if(b[L]&&b[L].options.groups.indexOf(c)>-1){for(;d--;)T[d]({clientX:H.clientX,clientY:H.clientY,target:a,rootEl:b});break}a=b}while(b=b.parentNode);Q||h(u,"display","")}},_onTouchMove:function(b){if(G){a.active||this._dragStarted(),this._appendGhost();var c=b.touches?b.touches[0]:b,d=c.clientX-G.clientX,e=c.clientY-G.clientY,f=b.touches?"translate3d("+d+"px,"+e+"px,0)":"translate("+d+"px,"+e+"px)";I=!0,H=c,h(u,"webkitTransform",f),h(u,"mozTransform",f),h(u,"msTransform",f),h(u,"transform",f),b.preventDefault()}},_appendGhost:function(){if(!u){var a,b=s.getBoundingClientRect(),c=h(s);u=s.cloneNode(!0),g(u,this.options.ghostClass,!1),g(u,this.options.fallbackClass,!0),h(u,"top",b.top-O(c.marginTop,10)),h(u,"left",b.left-O(c.marginLeft,10)),h(u,"width",b.width),h(u,"height",b.height),h(u,"opacity","0.8"),h(u,"position","fixed"),h(u,"zIndex","100000"),h(u,"pointerEvents","none"),this.options.fallbackOnBody&&N.body.appendChild(u)||w.appendChild(u),a=u.getBoundingClientRect(),h(u,"width",2*b.width-a.width),h(u,"height",2*b.height-a.height)}},_onDragStart:function(a,b){var c=a.dataTransfer,d=this.options;this._offUpEvents(),"clone"==F.pull&&(v=s.cloneNode(!0),h(v,"display","none"),w.insertBefore(v,s)),b?("touch"===b?(e(N,"touchmove",this._onTouchMove),e(N,"touchend",this._onDrop),e(N,"touchcancel",this._onDrop)):(e(N,"mousemove",this._onTouchMove),e(N,"mouseup",this._onDrop)),this._loopId=setInterval(this._emulateDragOver,50)):(c&&(c.effectAllowed="move",d.setData&&d.setData.call(this,c,s)),e(N,"drop",this),setTimeout(this._dragStarted,0))},_onDragOver:function(a){var d,e,f,g=this.el,i=this.options,j=i.group,l=j.put,o=F===j,p=i.sort;if(void 0!==a.preventDefault&&(a.preventDefault(),!i.dragoverBubble&&a.stopPropagation()),I=!0,F&&!i.disabled&&(o?p||(f=!w.contains(s)):F.pull&&l&&(F.name===j.name||l.indexOf&&~l.indexOf(F.name)))&&(void 0===a.rootEl||a.rootEl===this.el)){if(U(a,i,this.el),R)return;if(d=c(a.target,i.draggable,g),e=s.getBoundingClientRect(),f)return b(!0),void(v||x?w.insertBefore(s,v||x):p||w.appendChild(s));if(0===g.children.length||g.children[0]===u||g===a.target&&(d=n(g,a))){if(d){if(d.animated)return;r=d.getBoundingClientRect()}b(o),k(w,g,s,e,d,r)!==!1&&(s.contains(g)||(g.appendChild(s),t=g),this._animate(e,s),d&&this._animate(r,d))}else if(d&&!d.animated&&d!==s&&void 0!==d.parentNode[L]){A!==d&&(A=d,B=h(d),C=h(d.parentNode));var q,r=d.getBoundingClientRect(),y=r.right-r.left,z=r.bottom-r.top,D=/left|right|inline/.test(B.cssFloat+B.display)||"flex"==C.display&&0===C["flex-direction"].indexOf("row"),E=d.offsetWidth>s.offsetWidth,G=d.offsetHeight>s.offsetHeight,H=(D?(a.clientX-r.left)/y:(a.clientY-r.top)/z)>.5,J=d.nextElementSibling,K=k(w,g,s,e,d,r);if(K!==!1){if(R=!0,setTimeout(m,30),b(o),1===K||-1===K)q=1===K;else if(D){var M=s.offsetTop,N=d.offsetTop;q=M===N?d.previousElementSibling===s&&!E||H&&E:N>M}else q=J!==s&&!G||H&&G;s.contains(g)||(q&&!J?g.appendChild(s):d.parentNode.insertBefore(s,q?J:d)),t=s.parentNode,this._animate(e,s),this._animate(r,d)}}}},_animate:function(a,b){var c=this.options.animation;if(c){var d=b.getBoundingClientRect();h(b,"transition","none"),h(b,"transform","translate3d("+(a.left-d.left)+"px,"+(a.top-d.top)+"px,0)"),b.offsetWidth,h(b,"transition","all "+c+"ms"),h(b,"transform","translate3d(0,0,0)"),clearTimeout(b.animated),b.animated=setTimeout(function(){h(b,"transition",""),h(b,"transform",""),b.animated=!1},c)}},_offUpEvents:function(){var a=this.el.ownerDocument;f(N,"touchmove",this._onTouchMove),f(a,"mouseup",this._onDrop),f(a,"touchend",this._onDrop),f(a,"touchcancel",this._onDrop)},_onDrop:function(b){var c=this.el,d=this.options;clearInterval(this._loopId),clearInterval(J.pid),clearTimeout(this._dragStartTimer),f(N,"mousemove",this._onTouchMove),this.nativeDraggable&&(f(N,"drop",this),f(c,"dragstart",this._onDragStart)),this._offUpEvents(),b&&(I&&(b.preventDefault(),!d.dropBubble&&b.stopPropagation()),u&&u.parentNode.removeChild(u),s&&(this.nativeDraggable&&f(s,"dragend",this),l(s),g(s,this.options.ghostClass,!1),g(s,this.options.chosenClass,!1),w!==t?(E=p(s),E>=0&&(j(null,t,"sort",s,w,D,E),j(this,w,"sort",s,w,D,E),j(null,t,"add",s,w,D,E),j(this,w,"remove",s,w,D,E))):(v&&v.parentNode.removeChild(v),s.nextSibling!==x&&(E=p(s),E>=0&&(j(this,w,"update",s,w,D,E),j(this,w,"sort",s,w,D,E)))),a.active&&((null===E||-1===E)&&(E=D),j(this,w,"end",s,w,D,E),this.save())),w=s=t=u=x=v=y=z=G=H=I=E=A=B=F=a.active=null)},handleEvent:function(a){var b=a.type;"dragover"===b||"dragenter"===b?s&&(this._onDragOver(a),d(a)):("drop"===b||"dragend"===b)&&this._onDrop(a)},toArray:function(){for(var a,b=[],d=this.el.children,e=0,f=d.length,g=this.options;f>e;e++)a=d[e],c(a,g.draggable,this.el)&&b.push(a.getAttribute(g.dataIdAttr)||o(a));return b},sort:function(a){var b={},d=this.el;this.toArray().forEach(function(a,e){var f=d.children[e];c(f,this.options.draggable,d)&&(b[a]=f)},this),a.forEach(function(a){b[a]&&(d.removeChild(b[a]),d.appendChild(b[a]))})},save:function(){var a=this.options.store;a&&a.set(this)},closest:function(a,b){return c(a,b||this.options.draggable,this.el)},option:function(a,b){var c=this.options;return void 0===b?c[a]:(c[a]=b,void("group"===a&&V(c)))},destroy:function(){var a=this.el;a[L]=null,f(a,"mousedown",this._onTapStart),f(a,"touchstart",this._onTapStart),this.nativeDraggable&&(f(a,"dragover",this),f(a,"dragenter",this)),Array.prototype.forEach.call(a.querySelectorAll("[draggable]"),function(a){a.removeAttribute("draggable")}),T.splice(T.indexOf(this._onDragOver),1),this._onDrop(),this.el=a=null}},a.utils={on:e,off:f,css:h,find:i,is:function(a,b){return!!c(a,b,a)},extend:r,throttle:q,closest:c,toggleClass:g,index:p},a.create=function(b,c){return new a(b,c)},a.version="1.3.0",a});
\ No newline at end of file
diff --git a/public/Sortable-1.4.0/bower.json b/public/Sortable-1.4.0/bower.json
new file mode 100644
index 00000000..c934c0ec
--- /dev/null
+++ b/public/Sortable-1.4.0/bower.json
@@ -0,0 +1,35 @@
+{
+ "name": "Sortable",
+ "main": [
+ "Sortable.js",
+ "ng-sortable.js",
+ "knockout-sortable.js",
+ "react-sortable-mixin.js"
+ ],
+ "homepage": "http://rubaxa.github.io/Sortable/",
+ "authors": [
+ "RubaXa "
+ ],
+ "description": "Minimalist library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery.",
+ "keywords": [
+ "sortable",
+ "reorder",
+ "list",
+ "html5",
+ "drag",
+ "and",
+ "drop",
+ "dnd",
+ "web-components"
+ ],
+ "license": "MIT",
+ "ignore": [
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "polymer": "Polymer/polymer#~1.1.4",
+ }
+}
diff --git a/public/Sortable-1.4.0/component.json b/public/Sortable-1.4.0/component.json
new file mode 100644
index 00000000..20bfb914
--- /dev/null
+++ b/public/Sortable-1.4.0/component.json
@@ -0,0 +1,32 @@
+{
+ "name": "Sortable",
+ "main": "Sortable.js",
+ "version": "1.3.0",
+ "homepage": "http://rubaxa.github.io/Sortable/",
+ "repo": "RubaXa/Sortable",
+ "authors": [
+ "RubaXa "
+ ],
+ "description": "Minimalist library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery.",
+ "keywords": [
+ "sortable",
+ "reorder",
+ "list",
+ "html5",
+ "drag",
+ "and",
+ "drop",
+ "dnd"
+ ],
+ "license": "MIT",
+ "ignore": [
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+
+ "scripts": [
+ "Sortable.js"
+ ]
+}
diff --git a/public/Sortable-1.4.0/jquery.binding.js b/public/Sortable-1.4.0/jquery.binding.js
new file mode 100644
index 00000000..9e9f4b93
--- /dev/null
+++ b/public/Sortable-1.4.0/jquery.binding.js
@@ -0,0 +1,61 @@
+/**
+ * jQuery plugin for Sortable
+ * @author RubaXa
+ * @license MIT
+ */
+(function (factory) {
+ "use strict";
+
+ if (typeof define === "function" && define.amd) {
+ define(["jquery"], factory);
+ }
+ else {
+ /* jshint sub:true */
+ factory(jQuery);
+ }
+})(function ($) {
+ "use strict";
+
+
+ /* CODE */
+
+
+ /**
+ * jQuery plugin for Sortable
+ * @param {Object|String} options
+ * @param {..*} [args]
+ * @returns {jQuery|*}
+ */
+ $.fn.sortable = function (options) {
+ var retVal,
+ args = arguments;
+
+ this.each(function () {
+ var $el = $(this),
+ sortable = $el.data('sortable');
+
+ if (!sortable && (options instanceof Object || !options)) {
+ sortable = new Sortable(this, options);
+ $el.data('sortable', sortable);
+ }
+
+ if (sortable) {
+ if (options === 'widget') {
+ return sortable;
+ }
+ else if (options === 'destroy') {
+ sortable.destroy();
+ $el.removeData('sortable');
+ }
+ else if (typeof sortable[options] === 'function') {
+ retVal = sortable[options].apply(sortable, [].slice.call(args, 1));
+ }
+ else if (options in sortable.options) {
+ retVal = sortable.option.apply(sortable, args);
+ }
+ }
+ });
+
+ return (retVal === void 0) ? this : retVal;
+ };
+});
diff --git a/public/Sortable-1.4.0/package.json b/public/Sortable-1.4.0/package.json
new file mode 100644
index 00000000..a13735f8
--- /dev/null
+++ b/public/Sortable-1.4.0/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "sortablejs",
+ "exportName": "Sortable",
+ "version": "1.4.0",
+ "devDependencies": {
+ "grunt": "*",
+ "grunt-version": "*",
+ "grunt-exec": "*",
+ "grunt-contrib-jshint": "0.9.2",
+ "grunt-contrib-uglify": "*",
+ "spacejam": "*"
+ },
+ "description": "Minimalist JavaScript library for reorderable drag-and-drop lists on modern browsers and touch devices. No jQuery. Supports AngularJS and any CSS library, e.g. Bootstrap.",
+ "main": "Sortable.js",
+ "scripts": {
+ "test": "grunt"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/rubaxa/Sortable.git"
+ },
+ "keywords": [
+ "sortable",
+ "reorder",
+ "drag",
+ "meteor",
+ "angular",
+ "ng-sortable",
+ "react",
+ "mixin"
+ ],
+ "author": "Konstantin Lebedev ",
+ "license": "MIT",
+ "spm": {
+ "main": "Sortable.js",
+ "ignore": [
+ "meteor",
+ "st"
+ ]
+ }
+}
diff --git a/router_gen/routes.go b/router_gen/routes.go
index 69711903..cd6ad140 100644
--- a/router_gen/routes.go
+++ b/router_gen/routes.go
@@ -157,6 +157,9 @@ func buildPanelRoutes() {
View("routePanelThemesMenusEdit", "/panel/themes/menus/edit/", "extraData"),
View("routePanelThemesMenuItemEdit", "/panel/themes/menus/item/edit/", "extraData"),
Action("routePanelThemesMenuItemEditSubmit", "/panel/themes/menus/item/edit/submit/", "extraData"),
+ Action("routePanelThemesMenuItemCreateSubmit", "/panel/themes/menus/item/create/submit/"),
+ Action("routePanelThemesMenuItemDeleteSubmit", "/panel/themes/menus/item/delete/submit/", "extraData"),
+ Action("routePanelThemesMenuItemOrderSubmit", "/panel/themes/menus/item/order/edit/submit/", "extraData"),
View("routePanelPlugins", "/panel/plugins/"),
Action("routePanelPluginsActivate", "/panel/plugins/activate/", "extraData"),
diff --git a/templates/account_menu.html b/templates/account_menu.html
index fd5dc0ec..c9a5526c 100644
--- a/templates/account_menu.html
+++ b/templates/account_menu.html
@@ -9,7 +9,7 @@
-
+
{{/** TODO: Add an alerts page with pagination to go through alerts which either don't fit in the alerts drop-down or which have already been dismissed. Bear in mind though that dismissed alerts older than two weeks might be purged to save space and to speed up the database **/}}
diff --git a/templates/panel_themes_menus_items.html b/templates/panel_themes_menus_items.html
index c695ee4e..bdc1ffc4 100644
--- a/templates/panel_themes_menus_items.html
+++ b/templates/panel_themes_menus_items.html
@@ -1,6 +1,6 @@
{{template "header.html" . }}
-
+
@@ -16,14 +16,135 @@
{{lang "panel_themes_menus_head"}}
-
+
+
+
+
{{lang "panel_themes_menus_create_head"}}
+
+
+
{{template "footer.html" . }}
diff --git a/themes/cosora/public/panel.css b/themes/cosora/public/panel.css
index 0eeaa778..ea9799c6 100644
--- a/themes/cosora/public/panel.css
+++ b/themes/cosora/public/panel.css
@@ -180,6 +180,19 @@
content: "{{index .Phrases "panel_perms_default" }}";
}
+.panel_submitrow .rowitem {
+ display: flex;
+}
+.panel_submitrow .rowitem *:first-child {
+ margin-left: auto;
+}
+.panel_submitrow .rowitem *:last-child {
+ margin-right: auto;
+}
+.panel_submitrow .rowitem button {
+ margin-top: 0px;
+}
+
.colstack_graph_holder {
background-color: var(--element-background-color);
border: 1px solid var(--element-border-color);
diff --git a/themes/shadow/public/panel.css b/themes/shadow/public/panel.css
index 75659fda..4bc00aa8 100644
--- a/themes/shadow/public/panel.css
+++ b/themes/shadow/public/panel.css
@@ -68,6 +68,20 @@
padding-right: 2px;
}
+.panel_submitrow {
+ display: flex;
+}
+.panel_submitrow *:first-child {
+ margin-left: auto;
+}
+.panel_submitrow *:last-child {
+ margin-right: auto;
+}
+.panel_submitrow button {
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
.colstack_graph_holder {
background-color: var(--main-block-color);
padding: 10px;
diff --git a/themes/tempra-conflux/public/panel.css b/themes/tempra-conflux/public/panel.css
index 9c620a1d..2256c6bb 100644
--- a/themes/tempra-conflux/public/panel.css
+++ b/themes/tempra-conflux/public/panel.css
@@ -102,6 +102,9 @@
padding-left: 2px;
padding-right: 2px;
}
+.colstack_item + .panel_submitrow {
+ margin-bottom: 0px;
+}
.ct_chart {
padding-left: 10px;
diff --git a/themes/tempra-simple/public/panel.css b/themes/tempra-simple/public/panel.css
index 5e231730..4b0fd12e 100644
--- a/themes/tempra-simple/public/panel.css
+++ b/themes/tempra-simple/public/panel.css
@@ -118,9 +118,8 @@
display: none;
}
-/* TODO: Figure out how to handle this on the Control Panel */
-.footer {
- display: none;
+.colstack_item + .panel_submitrow {
+ margin-bottom: 0px;
}
#panel_word_filters .itemSeparator:before {