You can now re-order forums by dragging them in the Forum Manager.

Added some visual and textual hints to make it clearer that Menu Items and Forums can be dragged.
Added a hint to flush the page after pushing the <head>
Added the notice client template and pushNotice client function.

Used a pointer instead of a struct for AnalyticsTimeRange in the analytics routes.
Caught a potential missing error check in InitPhrases.
Use struct{} instead of bool in some of the user mapping maps for WebSockets to save space.
Added the buildUserExprs function to eliminate a bit of duplication.
Fixed a typo in ForumsEdit where it referenced a non-existent notice phrase.
Client hooks can now sort of return things.
Panel phrases are now fetched by init.js, but only in the control panel.
Reduced the number of unused phrases loaded in both the front-end and the control panel.

Plugin hyperdrive should handle Gzip better now.

Added the panel.ForumsOrderSubmit route.

Added the panel_hints_reorder phrase.
Moved the panel_forums phrases into the panel. namespace.
Added the panel.forums_order_updated phrase.
Renamed the panel_themes_menus_item_edit_button_aria phrase to panel_themes_menus_items_edit_button_aria
Renamed the panel_themes_menus_item_delete_button_aria phrase to panel_themes_menus_items_delete_button_aria
Added the panel_themes_menus_items_update_button phrase.

You will need to run the patcher / updater for this commit.
This commit is contained in:
Azareal 2019-04-27 16:32:26 +10:00
parent 2964cd767d
commit 27a4a74840
31 changed files with 798 additions and 511 deletions

View File

@ -182,6 +182,7 @@ func createTables(adapter qgen.Adapter) error {
tblColumn{"name", "varchar", 100, false, false, ""},
tblColumn{"desc", "varchar", 200, false, false, ""},
tblColumn{"active", "boolean", 0, false, false, "1"},
tblColumn{"order", "int", 0, false, false, "0"},
tblColumn{"topicCount", "int", 0, false, false, "0"},
tblColumn{"preset", "varchar", 100, false, false, "''"},
tblColumn{"parentID", "int", 0, false, false, "0"},
@ -427,6 +428,7 @@ func createTables(adapter qgen.Adapter) error {
[]tblColumn{
tblColumn{"uname", "varchar", 180, false, false, ""},
tblColumn{"default", "boolean", 0, false, false, "0"},
//tblColumn{"profileUserVars", "text", 0, false, false, "''"},
},
[]tblKey{
tblKey{"uname", "unique"},

View File

@ -1,7 +1,7 @@
package common
//import "fmt"
import (
//"log"
"database/sql"
"errors"
"strconv"
@ -28,6 +28,7 @@ type Forum struct {
Name string
Desc string
Active bool
Order int
Preset string
ParentID int
ParentType string
@ -135,8 +136,22 @@ func (sf SortForum) Len() int {
func (sf SortForum) Swap(i, j int) {
sf[i], sf[j] = sf[j], sf[i]
}
/*func (sf SortForum) Less(i,j int) bool {
l := sf.less(i,j)
if l {
log.Printf("%s is less than %s. order: %d. id: %d.",sf[i].Name, sf[j].Name, sf[i].Order, sf[i].ID)
} else {
log.Printf("%s is not less than %s. order: %d. id: %d.",sf[i].Name, sf[j].Name, sf[i].Order, sf[i].ID)
}
return l
}*/
func (sf SortForum) Less(i, j int) bool {
return sf[i].ID < sf[j].ID
if sf[i].Order < sf[j].Order {
return true
} else if sf[i].Order == sf[j].Order {
return sf[i].ID < sf[j].ID
}
return false
}
// ! Don't use this outside of tests and possibly template_init.go

View File

@ -42,6 +42,7 @@ type ForumStore interface {
//GetChildren(parentID int, parentType string) ([]*Forum,error)
//GetFirstChild(parentID int, parentType string) (*Forum,error)
Create(forumName string, forumDesc string, active bool, preset string) (int, error)
UpdateOrder(updateMap map[int]int) error
GlobalCount() int
}
@ -66,6 +67,7 @@ type MemoryForumStore struct {
updateCache *sql.Stmt
addTopics *sql.Stmt
removeTopics *sql.Stmt
updateOrder *sql.Stmt
}
// NewMemoryForumStore gives you a new instance of MemoryForumStore
@ -73,17 +75,19 @@ func NewMemoryForumStore() (*MemoryForumStore, error) {
acc := qgen.NewAcc()
// TODO: Do a proper delete
return &MemoryForumStore{
get: acc.Select("forums").Columns("name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid = ?").Prepare(),
getAll: acc.Select("forums").Columns("fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("fid ASC").Prepare(),
get: acc.Select("forums").Columns("name, desc, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid = ?").Prepare(),
getAll: acc.Select("forums").Columns("fid, name, desc, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("order ASC, fid ASC").Prepare(),
delete: acc.Update("forums").Set("name= '', active = 0").Where("fid = ?").Prepare(),
create: acc.Insert("forums").Columns("name, desc, active, preset").Fields("?,?,?,?").Prepare(),
count: acc.Count("forums").Where("name != ''").Prepare(),
updateCache: acc.Update("forums").Set("lastTopicID = ?, lastReplyerID = ?").Where("fid = ?").Prepare(),
addTopics: acc.Update("forums").Set("topicCount = topicCount + ?").Where("fid = ?").Prepare(),
removeTopics: acc.Update("forums").Set("topicCount = topicCount - ?").Where("fid = ?").Prepare(),
updateOrder: acc.Update("forums").Set("order = ?").Where("fid = ?").Prepare(),
}, acc.FirstError()
}
// TODO: Rename to ReloadAll?
// TODO: Add support for subforums
func (mfs *MemoryForumStore) LoadForums() error {
var forumView []*Forum
@ -103,7 +107,7 @@ func (mfs *MemoryForumStore) LoadForums() error {
var i = 0
for ; rows.Next(); i++ {
forum := &Forum{ID: 0, Active: true, Preset: "all"}
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil {
return err
}
@ -161,7 +165,7 @@ func (mfs *MemoryForumStore) Get(id int) (*Forum, error) {
fint, ok := mfs.forums.Load(id)
if !ok || fint.(*Forum).Name == "" {
var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil {
return forum, err
}
@ -178,7 +182,7 @@ func (mfs *MemoryForumStore) Get(id int) (*Forum, error) {
func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) {
var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil {
return nil, err
}
@ -206,7 +210,7 @@ func (mfs *MemoryForumStore) BulkGetCopy(ids []int) (forums []Forum, err error)
func (mfs *MemoryForumStore) Reload(id int) error {
var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Order, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil {
return err
}
@ -348,6 +352,17 @@ func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active b
return fid, nil
}
// TODO: Make this atomic, maybe with a transaction?
func (s *MemoryForumStore) UpdateOrder(updateMap map[int]int) error {
for fid, order := range updateMap {
_, err := s.updateOrder.Exec(order, fid)
if err != nil {
return err
}
}
return s.LoadForums()
}
// ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x
// Length returns the number of forums in the memory cache
func (mfs *MemoryForumStore) Length() (length int) {

View File

@ -39,6 +39,7 @@ type LevelPhrases struct {
type LanguagePack struct {
Name string
IsoCode string
//LastUpdated string
// Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent.
Levels LevelPhrases
@ -70,6 +71,9 @@ func InitPhrases(lang string) error {
if f.IsDir() {
return nil
}
if err != nil {
return err
}
data, err := ioutil.ReadFile(path)
if err != nil {

View File

@ -158,6 +158,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
header.AddPreScriptAsync("template_" + name + tname + ".js")
}
addPreScript("alert")
addPreScript("notice")
return header, stats, nil
}
@ -267,6 +268,7 @@ func PrepResources(user *User, header *Header, theme *Theme) {
addPreScript("topics_topic")
addPreScript("paginator")
addPreScript("alert")
addPreScript("notice")
if user.Loggedin {
addPreScript("topic_c_edit_post")
addPreScript("topic_c_attach_item")

View File

@ -481,6 +481,8 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri
tmpls.AddStd("topic_c_attach_item", "common.TopicCAttachItem", TopicCAttachItem{ID: 1, ImgSrc: "", Path: "", FullPath: ""})
tmpls.AddStd("notice", "string", "nonono")
var dirPrefix = "./tmpl_client/"
var writeTemplate = func(name string, content string) {
log.Print("Writing template '" + name + "'")
@ -711,6 +713,10 @@ func initDefaultTmplFuncMap() {
return ""
}
fmap["flush"] = func() interface{} {
return nil
}
DefaultTemplateFuncMap = fmap
}

View File

@ -74,6 +74,7 @@ type CTemplateSet struct {
logger *log.Logger
loggerf *os.File
lang string
}
func NewCTemplateSet(in string) *CTemplateSet {
@ -112,9 +113,11 @@ func NewCTemplateSet(in string) *CTemplateSet {
"scope": true,
"dyntmpl": true,
"index": true,
"flush": true,
},
logger: log.New(f, "", log.LstdFlags),
loggerf: f,
lang:in,
}
}
@ -445,6 +448,16 @@ func (c *CTemplateSet) compile(name string, content string, expects string, expe
return errors.New("invalid page struct value")
}
`
if c.lang == "normal" {
fout += `var iw http.ResponseWriter
gzw, ok := w.(common.GzipResponseWriter)
if ok {
iw = gzw.ResponseWriter
}
_ = iw
`
}
if len(c.langIndexToName) > 0 {
fout += "var plist = phrases.GetTmplPhrasesBytes(" + fname + "_tmpl_phrase_id)\n"
}
@ -587,16 +600,7 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) {
c.detail("Expression:", expr)
// Simple member / guest optimisation for now
// TODO: Expand upon this
var userExprs = []string{
con.RootHolder + ".CurrentUser.Loggedin",
con.RootHolder + ".CurrentUser.IsSuperMod",
con.RootHolder + ".CurrentUser.IsAdmin",
}
var negUserExprs = []string{
"!" + con.RootHolder + ".CurrentUser.Loggedin",
"!" + con.RootHolder + ".CurrentUser.IsSuperMod",
"!" + con.RootHolder + ".CurrentUser.IsAdmin",
}
userExprs, negUserExprs := buildUserExprs(con.RootHolder)
if c.guestOnly {
c.detail("optimising away member branch")
if inSlice(userExprs, expr) {
@ -1170,6 +1174,16 @@ ArgLoop:
out += "if err != nil {\nreturn err\n}\n}\n"
literal = true
break ArgLoop
case "flush":
if c.lang == "js" {
continue
}
out = "if fl, ok := iw.(http.Flusher); ok {\n"
out += "fl.Flush()\n"
out += "}\n"
literal = true
c.importMap["net/http"] = "net/http"
break ArgLoop
default:
c.detail("Variable!")
if len(node.Args) > (pos + 1) {
@ -1391,6 +1405,20 @@ func (c *CTemplateSet) retCall(name string, params ...interface{}) {
c.detail("returned from " + name + " => (" + pstr + ")")
}
func buildUserExprs(holder string) ([]string,[]string) {
var userExprs = []string{
holder + ".CurrentUser.Loggedin",
holder + ".CurrentUser.IsSuperMod",
holder + ".CurrentUser.IsAdmin",
}
var negUserExprs = []string{
"!" + holder + ".CurrentUser.Loggedin",
"!" + holder + ".CurrentUser.IsSuperMod",
"!" + holder + ".CurrentUser.IsAdmin",
}
return userExprs, negUserExprs
}
func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.Value, assLines string, onEnd func(string) string) {
c.dumpCall("compileVarSub", con, varname, val, assLines, onEnd)
defer c.retCall("compileVarSub")
@ -1438,16 +1466,7 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V
// TODO: Take c.memberOnly into account
// TODO: Make this a template fragment so more optimisations can be applied to this
// TODO: De-duplicate this logic
var userExprs = []string{
con.RootHolder + ".CurrentUser.Loggedin",
con.RootHolder + ".CurrentUser.IsSuperMod",
con.RootHolder + ".CurrentUser.IsAdmin",
}
var negUserExprs = []string{
"!" + con.RootHolder + ".CurrentUser.Loggedin",
"!" + con.RootHolder + ".CurrentUser.IsSuperMod",
"!" + con.RootHolder + ".CurrentUser.IsAdmin",
}
userExprs, negUserExprs := buildUserExprs(con.RootHolder)
if c.guestOnly {
c.detail("optimising away member branch")
if inSlice(userExprs, varname) {
@ -1804,4 +1823,4 @@ func (c *CTemplateSet) error(args ...interface{}) {
func (c *CTemplateSet) critical(args ...interface{}) {
c.logger.Println(args...)
}
}

View File

@ -33,8 +33,8 @@ var errWsNouser = errors.New("This user isn't connected via WebSockets")
func init() {
adminStatsWatchers = make(map[*websocket.Conn]*WSUser)
topicListWatchers = make(map[*WSUser]bool)
topicWatchers = make(map[int]map[*WSUser]bool)
topicListWatchers = make(map[*WSUser]struct{})
topicWatchers = make(map[int]map[*WSUser]struct{})
}
//easyjson:json
@ -130,7 +130,7 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
// TODO: Optimise this to reduce the amount of contention
case page == "/topics/":
topicListMutex.Lock()
topicListWatchers[wsUser] = true
topicListWatchers[wsUser] = struct{}{}
topicListMutex.Unlock()
// TODO: Evict from page when permissions change? Or check user perms every-time before sending data?
case strings.HasPrefix(page, "/topic/"):
@ -169,9 +169,9 @@ func wsPageResponses(wsUser *WSUser, conn *websocket.Conn, page string) {
topicMutex.Lock()
_, ok := topicWatchers[topic.ID]
if !ok {
topicWatchers[topic.ID] = make(map[*WSUser]bool)
topicWatchers[topic.ID] = make(map[*WSUser]struct{})
}
topicWatchers[topic.ID][wsUser] = true
topicWatchers[topic.ID][wsUser] = struct{}{}
topicMutex.Unlock()
case page == "/panel/":
if !wsUser.User.IsSuperMod {
@ -243,9 +243,9 @@ func wsLeavePage(wsUser *WSUser, conn *websocket.Conn, page string) {
// TODO: Abstract this
// TODO: Use odd-even sharding
var topicListWatchers map[*WSUser]bool
var topicListWatchers map[*WSUser]struct{}
var topicListMutex sync.RWMutex
var topicWatchers map[int]map[*WSUser]bool // map[tid]watchers
var topicWatchers map[int]map[*WSUser]struct{} // map[tid]watchers
var topicMutex sync.RWMutex
var adminStatsWatchers map[*websocket.Conn]*WSUser
var adminStatsMutex sync.RWMutex

View File

@ -35,6 +35,7 @@ func deactivateHdrive(plugin *c.Plugin) {
type Hyperspace struct {
topicList atomic.Value
gzipTopicList atomic.Value
}
func newHyperspace() *Hyperspace {
@ -48,10 +49,7 @@ func tickHdriveWol(args ...interface{}) (skip bool, rerr c.RouteError) {
return tickHdrive(args)
}
// TODO: Find a better way of doing this
func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
c.DebugLog("Refueling...")
w := httptest.NewRecorder()
func dummyReqHdrive() http.ResponseWriter {
req := httptest.NewRequest("get", "/topics/", bytes.NewReader(nil))
user := c.GuestUser
@ -68,17 +66,48 @@ func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
}
if w.Code != 200 {
c.LogWarning(err)
return false, nil
}
return w
}
// TODO: Find a better way of doing this
func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
c.DebugLog("Refueling...")
w := httptest.NewRecorder()
dummyReqHdrive(w)
buf := new(bytes.Buffer)
buf.ReadFrom(w.Result().Body)
hyperspace.topicList.Store(buf.Bytes())
w = httptest.NewRecorder()
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
gz := gzip.NewWriter(w)
w = c.GzipResponseWriter{Writer: gz, ResponseWriter: w}
dummyReqHdrive(w)
buf = new(bytes.Buffer)
buf.ReadFrom(w.Result().Body)
hyperspace.gzipTopicList.Store(buf.Bytes())
if w.Header().Get("Content-Encoding") == "gzip" {
gz.Close()
}
return false, nil
}
func jumpHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
tList := hyperspace.topicList.Load().([]byte)
var tList []byte
w := args[0].(http.ResponseWriter)
_, ok := w.(c.GzipResponseWriter)
if ok {
tList = hyperspace.gzipTopicList.Load().([]byte)
} else {
tList = hyperspace.topicList.Load().([]byte)
}
if len(tList) == 0 {
c.DebugLog("no topiclist in hyperspace")
return false, nil
@ -101,7 +130,6 @@ func jumpHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
//c.DebugLog
c.DebugLog("Successful jump")
w := args[0].(http.ResponseWriter)
header := args[3].(*c.Header)
routes.FootHeaders(w, header)
w.Write(tList)

File diff suppressed because it is too large Load Diff

View File

@ -715,6 +715,8 @@
"option_yes":"Yes",
"option_no":"No",
"panel_hints_reorder":"Drag to change the order",
"panel_back_to_site":"Back to Site",
"panel_welcome":"Welcome ",
"panel_menu_head":"Control Panel",
@ -769,22 +771,24 @@
"panel_user_group":"Group",
"panel_user_update_button":"Update User",
"panel_forums_head":"Forums",
"panel_forums_hidden":"Hidden",
"panel_forums_edit_button_tooltip":"Edit Forum",
"panel_forums_edit_button_aria":"Edit Forum",
"panel_forums_update_button":"Update",
"panel_forums_delete_button_tooltip":"Delete Forum",
"panel_forums_delete_button_aria":"Delete Forum",
"panel_forums_full_edit_button":"Full Edit",
"panel_forums_create_head":"Add Forum",
"panel_forums_create_name_label":"Name",
"panel_forums_create_name":"Super Secret Forum",
"panel_forums_create_description_label":"Description",
"panel_forums_create_description":"Where all the super secret stuff happens",
"panel_forums_active_label":"Active",
"panel_forums_preset_label":"Preset",
"panel_forums_create_button":"Add Forum",
"panel.forums_head":"Forums",
"panel.forums_hidden":"Hidden",
"panel.forums_edit_button_tooltip":"Edit Forum",
"panel.forums_edit_button_aria":"Edit Forum",
"panel.forums_update_button":"Update",
"panel.forums_delete_button_tooltip":"Delete Forum",
"panel.forums_delete_button_aria":"Delete Forum",
"panel.forums_full_edit_button":"Full Edit",
"panel.forums_create_head":"Add Forum",
"panel.forums_create_name_label":"Name",
"panel.forums_create_name":"Super Secret Forum",
"panel.forums_create_description_label":"Description",
"panel.forums_create_description":"Where all the super secret stuff happens",
"panel.forums_active_label":"Active",
"panel.forums_preset_label":"Preset",
"panel.forums_create_button":"Add Forum",
"panel.forums_update_order_button":"Update Order",
"panel.forums_order_updated":"The forums have been successfully updated",
"panel_forum_head_suffix":" Forum",
"panel_forum_name":"Name",
@ -942,8 +946,9 @@
"panel_themes_menus_head":"Menus",
"panel_themes_menus_main":"Main Menu",
"panel_themes_menus_items_head":"Menu Items",
"panel_themes_menus_item_edit_button_aria":"Edit menu item",
"panel_themes_menus_item_delete_button_aria":"Delete menu item",
"panel_themes_menus_items_edit_button_aria":"Edit menu item",
"panel_themes_menus_items_delete_button_aria":"Delete menu item",
"panel_themes_menus_items_update_button":"Update Order",
"panel_themes_menus_edit_head":"Menu Editor",
"panel_themes_menus_create_head":"Create Menu Item",

View File

@ -30,6 +30,7 @@ func init() {
addPatch(15, patch15)
addPatch(16, patch16)
addPatch(17, patch17)
addPatch(18, patch18)
}
func patch0(scanner *bufio.Scanner) (err error) {
@ -588,3 +589,7 @@ func patch17(scanner *bufio.Scanner) error {
return err
})
}
func patch18(scanner *bufio.Scanner) error {
return execStmt(qgen.Builder.AddColumn("forums", tblColumn{"order", "int", 0, false, false, "0"}, nil))
}

View File

@ -14,6 +14,14 @@ var wsBackoff = 0;
// Topic move
var forumToMoveTo = 0;
function pushNotice(msg) {
let aBox = document.getElementsByClassName("alertbox")[0];
let div = document.createElement('div');
div.innerHTML = Template_notice(msg).trim();
aBox.appendChild(div);
runInitHook("after_notice");
}
// TODO: Write a friendlier error handler which uses a .notice or something, we could have a specialised one for alerts
function ajaxError(xhr,status,errstr) {
console.log("The AJAX request failed");

View File

@ -28,9 +28,9 @@ function runHook(name, ...args) {
console.log("Running hook '"+name+"'");
let hook = hooks[name];
for (const index in hook) {
hook[index](...args);
}
let ret;
for (const index in hook) ret = hook[index](...args);
return ret;
}
function addHook(name, callback) {
@ -40,15 +40,14 @@ function addHook(name, callback) {
// InitHooks are slightly special, as if they are run, then any adds after the initial run will run immediately, this is to deal with the async nature of script loads
function runInitHook(name, ...args) {
runHook(name,...args);
let ret = runHook(name,...args);
ranInitHooks[name] = true;
return ret;
}
function addInitHook(name, callback) {
addHook(name, callback);
if(name in ranInitHooks) {
callback();
}
if(name in ranInitHooks) callback();
}
// Temporary hack for templates
@ -175,14 +174,14 @@ function RelativeTime(date) {
return date;
}
function initPhrases(loggedIn) {
function initPhrases(loggedIn, panel = false) {
console.log("in initPhrases")
console.log("tmlInits:",tmplInits)
let e = "";
if(loggedIn) {
e = ",topic"
}
fetchPhrases("status,topic_list,alerts,paginator,analytics"+e) // TODO: Break this up?
if(loggedIn && !panel) e = ",topic_list,topic";
else if(panel) e = ",analytics,panel"; // TODO: Request phrases for just one section of the control panel?
else e = ",topic_list";
fetchPhrases("status,alerts,paginator"+e) // TODO: Break this up?
}
function fetchPhrases(plist) {
@ -219,15 +218,18 @@ function fetchPhrases(plist) {
(() => {
runInitHook("pre_iife");
let loggedIn = document.head.querySelector("[property='x-loggedin']").content == "true";
let panel = window.location.pathname.startsWith("/panel/");
if(!window.location.pathname.startsWith("/panel/")) {
let toLoad = 2;
// TODO: Shunt this into loggedIn if there aren't any search and filter widgets?
let q = (f) => {
toLoad--;
if(toLoad===0) initPhrases(loggedIn);
if(f) throw("template function not found");
};
let toLoad = 1;
// TODO: Shunt this into loggedIn if there aren't any search and filter widgets?
let q = (f) => {
toLoad--;
if(toLoad===0) initPhrases(loggedIn,panel);
if(f) throw("template function not found");
};
if(!panel) {
toLoad += 2;
if(loggedIn) {
toLoad += 2;
notifyOnScriptW("template_topic_c_edit_post", () => q(!Template_topic_c_edit_post));
@ -235,9 +237,8 @@ function fetchPhrases(plist) {
}
notifyOnScriptW("template_topics_topic", () => q(!Template_topics_topic));
notifyOnScriptW("template_paginator", () => q(!Template_paginator));
} else {
initPhrases(false);
}
notifyOnScriptW("template_notice", () => q(!Template_notice));
if(loggedIn) {
fetch("/api/me/")

59
public/panel_forums.js Normal file
View File

@ -0,0 +1,59 @@
(() => {
addInitHook("end_init", () => {
formVars = {
'forum_active': ['Hide','Show'],
'forum_preset': ['all','announce','members','staff','admins','archive','custom']
};
var forums = {};
let items = document.getElementsByClassName("panel_forum_item");
for(let i = 0; item = items[i]; i++) forums[i] = item.getAttribute("data-fid");
console.log("forums:",forums);
Sortable.create(document.getElementById("panel_forums"), {
sort: true,
onEnd: (evt) => {
console.log("pre forums: ", forums)
console.log("evt: ", evt)
let oldFid = forums[evt.newIndex];
forums[evt.oldIndex] = oldFid;
let newFid = evt.item.getAttribute("data-fid");
console.log("newFid: ", newFid);
forums[evt.newIndex] = newFid;
console.log("post forums: ", forums);
}
});
document.getElementById("panel_forums_order_button").addEventListener("click", () => {
let req = new XMLHttpRequest();
if(!req) {
console.log("Failed to create request");
return false;
}
req.onreadystatechange = () => {
try {
if(req.readyState!==XMLHttpRequest.DONE) return;
// TODO: Signal the error with a notice
if(req.status!==200) return;
let resp = JSON.parse(req.responseText);
console.log("resp: ", resp);
// TODO: Should we move other notices into TmplPhrases like this one?
pushNotice(phraseBox["panel"]["panel.forums_order_updated"]);
if(resp.success==1) return;
} catch(ex) {
console.error("exception: ", ex)
}
console.trace();
}
// ? - Is encodeURIComponent the right function for this?
req.open("POST","/panel/forums/order/edit/submit/?session=" + encodeURIComponent(me.User.Session));
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
let items = "";
for(let i = 0; item = forums[i];i++) items += item+",";
if(items.length > 0) items = items.slice(0,-1);
req.send("js=1&amp;items={"+items+"}");
});
});
})();

View File

@ -151,6 +151,7 @@ func panelRoutes() *RouteGroup {
Action("panel.ForumsCreateSubmit", "/panel/forums/create/"),
Action("panel.ForumsDelete", "/panel/forums/delete/", "extraData"),
Action("panel.ForumsDeleteSubmit", "/panel/forums/delete/submit/", "extraData"),
Action("panel.ForumsOrderSubmit", "/panel/forums/order/edit/submit/"),
View("panel.ForumsEdit", "/panel/forums/edit/", "extraData"),
Action("panel.ForumsEditSubmit", "/panel/forums/edit/submit/", "extraData"),
Action("panel.ForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extraData"),

View File

@ -145,6 +145,8 @@ var phraseWhitelist = []string{
"alerts",
"paginator",
"analytics",
"panel", // We're going to handle this specially below as this is a security boundary
}
func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
@ -199,13 +201,22 @@ func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
var ok = false
for _, item := range phraseWhitelist {
if strings.HasPrefix(positive, item) {
ok = true
// TODO: Break this down into smaller security boundaries based on control panel sections?
if strings.HasPrefix(positive,"panel") {
if user.IsSuperMod {
ok = true
w.Header().Set("Cache-Control", "private")
}
} else {
ok = true
}
break
}
}
if !ok {
return c.PreErrorJS("Outside of phrase prefix whitelist", w, r)
}
pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positive)
if !ok {
return c.PreErrorJS("No such prefix", w, r)
@ -219,13 +230,22 @@ func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
var ok = false
for _, item := range phraseWhitelist {
if strings.HasPrefix(positives[0], item) {
ok = true
// TODO: Break this down into smaller security boundaries based on control panel sections?
if strings.HasPrefix(positives[0],"panel") {
if user.IsSuperMod {
ok = true
w.Header().Set("Cache-Control", "private")
}
} else {
ok = true
}
break
}
}
if !ok {
return c.PreErrorJS("Outside of phrase prefix whitelist", w, r)
}
pPhrases, ok := phrases.GetTmplPhrasesByPrefix(positives[0])
if !ok {
return c.PreErrorJS("No such prefix", w, r)

View File

@ -22,7 +22,8 @@ type AnalyticsTimeRange struct {
Range string
}
func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err error) {
func analyticsTimeRange(rawTimeRange string) (*AnalyticsTimeRange, error) {
timeRange := &AnalyticsTimeRange{}
timeRange.Quantity = 6
timeRange.Unit = "hour"
timeRange.Slices = 12
@ -78,7 +79,7 @@ func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err
return timeRange, nil
}
func analyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
func analyticsTimeRangeToLabelList(timeRange *AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
viewMap = make(map[int64]int64)
var currentTime = time.Now().Unix()
for i := 1; i <= timeRange.Slices; i++ {

View File

@ -19,6 +19,8 @@ func Forums(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
if !user.Perms.ManageForums {
return c.NoPermissions(w, r, user)
}
basePage.Header.AddScript("Sortable-1.4.0/Sortable.min.js")
basePage.Header.AddScriptAsync("panel_forums.js")
// TODO: Paginate this?
var forumList []interface{}
@ -130,6 +132,31 @@ func ForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user c.User, sfi
return nil
}
func ForumsOrderSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
_, ferr := c.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
}
isJs := (r.PostFormValue("js") == "1")
if !user.Perms.ManageForums {
return c.NoPermissionsJSQ(w, r, user, isJs)
}
sitems := strings.TrimSuffix(strings.TrimPrefix(r.PostFormValue("items"), "{"), "}")
//fmt.Printf("sitems: %+v\n", sitems)
var updateMap = make(map[int]int)
for index, sfid := range strings.Split(sitems, ",") {
fid, err := strconv.Atoi(sfid)
if err != nil {
return c.LocalErrorJSQ("Invalid integer in forum list", w, r, user, isJs)
}
updateMap[fid] = index
}
c.Forums.UpdateOrder(updateMap)
return successRedirect("/panel/forums/", w, r, isJs)
}
func ForumsEdit(w http.ResponseWriter, r *http.Request, user c.User, sfid string) c.RouteError {
basePage, ferr := buildBasePage(w, r, &user, "edit_forum", "forums")
if ferr != nil {
@ -333,7 +360,7 @@ func ForumsEditPermsAdvance(w http.ResponseWriter, r *http.Request, user c.User,
addNameLangToggle("MoveTopic", forumPerms.MoveTopic)
if r.FormValue("updated") == "1" {
basePage.AddNotice("panel_forums_perms_updated")
basePage.AddNotice("panel_forum_perms_updated")
}
pi := c.PanelEditForumGroupPage{basePage, forum.ID, gid, forum.Name, forum.Desc, forum.Active, forum.Preset, formattedPermList}

View File

@ -3,6 +3,7 @@ CREATE TABLE [forums] (
[name] nvarchar (100) not null,
[desc] nvarchar (200) not null,
[active] bit DEFAULT 1 not null,
[order] int DEFAULT 0 not null,
[topicCount] int DEFAULT 0 not null,
[preset] nvarchar (100) DEFAULT '' not null,
[parentID] int DEFAULT 0 not null,

View File

@ -3,6 +3,7 @@ CREATE TABLE `forums` (
`name` varchar(100) not null,
`desc` varchar(200) not null,
`active` boolean DEFAULT 1 not null,
`order` int DEFAULT 0 not null,
`topicCount` int DEFAULT 0 not null,
`preset` varchar(100) DEFAULT '' not null,
`parentID` int DEFAULT 0 not null,

View File

@ -3,6 +3,7 @@ CREATE TABLE "forums" (
`name` varchar (100) not null,
`desc` varchar (200) not null,
`active` boolean DEFAULT 1 not null,
`order` int DEFAULT 0 not null,
`topicCount` int DEFAULT 0 not null,
`preset` varchar (100) DEFAULT '' not null,
`parentID` int DEFAULT 0 not null,

View File

@ -7,7 +7,7 @@
{{range .Header.PreScriptsAsync}}
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
<meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" />
<script type="text/javascript" src="/static/init.js?i=3"></script>
<script type="text/javascript" src="/static/init.js?i=4"></script>
{{range .Header.ScriptsAsync}}
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
<script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
@ -25,7 +25,7 @@
{{if .GoogSiteVerify}}<meta name="google-site-verification" content="{{.GoogSiteVerify}}" />{{end}}
</head>
<body>
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}
{{if not .CurrentUser.IsSuperMod}}<style>.supermod_only { display: none !important; }</style>{{end}}{{flush}}
<div id="container" class="container">
{{/**<!--<div class="navrow">-->**/}}
<div class="left_of_nav">{{dock "leftOfNav" .Header }}</div>

View File

@ -2,60 +2,63 @@
<div class="colstack panel_stack">
{{template "panel_menu.html" . }}
<script>var formVars = {
'forum_active': ['Hide','Show'],
'forum_preset': ['all','announce','members','staff','admins','archive','custom']};
</script>
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_forums_head"}}</h1></div>
<div class="rowitem">
<h1>{{lang "panel.forums_head"}}</h1>
<h2 class="hguide">{{lang "panel_hints_reorder"}}</h2>
</div>
</div>
<div id="panel_forums" class="colstack_item rowlist">
{{range .ItemList}}
<div class="rowitem editable_parent">
<div data-fid="{{.ID}}" class="rowitem editable_parent panel_forum_item{{if not .Desc}} forum_no_desc{{end}}">
<span class="grip"></span>
<span id="panel_forums_left_box">
{{/** TODO: Make sure the forum_active_name class is set and unset when the activity status of this forum is changed **/}}
<a data-field="forum_name" data-type="text" class="editable_block forum_name{{if not .Active}} forum_active_name{{end}}">{{.Name}}</a>
<br /><span data-field="forum_desc" data-type="text" class="editable_block forum_desc rowsmall">{{.Desc}}</span>
</span>
<span class="panel_floater">
<span data-field="forum_active" data-type="list" class="panel_tag editable_block forum_active {{if .Active}}forum_active_Show" data-value="1{{else}}forum_active_Hide" data-value="0{{end}}" title="{{lang "panel_forums_hidden"}}"></span>
<span data-field="forum_active" data-type="list" class="panel_tag editable_block forum_active forum_active_{{if .Active}}Show" data-value="1{{else}}Hide" data-value="0{{end}}" title="{{lang "panel.forums_hidden"}}"></span>
<span data-field="forum_preset" data-type="list" data-value="{{.Preset}}" class="panel_tag editable_block forum_preset forum_preset_{{.Preset}}" title="{{.PresetLang}}"></span>
</span>
<span class="panel_buttons">
<a class="panel_tag edit_fields hide_on_edit panel_right_button edit_button" title="{{lang "panel_forums_edit_button_tooltip"}}" aria-label="{{lang "panel_forums_edit_button_aria"}}"></a>
<a class="panel_right_button has_inner_button show_on_edit" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit' type='submit'>{{lang "panel_forums_update_button"}}</button></a>
{{if gt .ID 1}}<a href="/panel/forums/delete/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button hide_on_edit delete_button" title="{{lang "panel_forums_delete_button_tooltip"}}" aria-label="{{lang "panel_forums_delete_button_aria"}}"></a>{{end}}
<a href="/panel/forums/edit/{{.ID}}" class="panel_tag panel_right_button has_inner_button show_on_edit"><button>{{lang "panel_forums_full_edit_button"}}</button></a>
<a class="panel_tag edit_fields hide_on_edit panel_right_button edit_button" title="{{lang "panel.forums_edit_button_tooltip"}}" aria-label="{{lang "panel.forums_edit_button_aria"}}"></a>
<a class="panel_right_button has_inner_button show_on_edit" href="/panel/forums/edit/submit/{{.ID}}"><button class='panel_tag submit_edit' type='submit'>{{lang "panel.forums_update_button"}}</button></a>
{{if gt .ID 1}}<a href="/panel/forums/delete/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button hide_on_edit delete_button" title="{{lang "panel.forums_delete_button_tooltip"}}" aria-label="{{lang "panel.forums_delete_button_aria"}}"></a>{{end}}
<a href="/panel/forums/edit/{{.ID}}" class="panel_tag panel_right_button has_inner_button show_on_edit"><button>{{lang "panel.forums_full_edit_button"}}</button></a>
</span>
</div>
{{end}}
</div>
<div class="colstack_item rowlist panel_submitrow">
<div class="rowitem"><button id="panel_forums_order_button" class="formbutton">{{lang "panel.forums_update_order_button"}}</button></div>
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_forums_create_head"}}</h1></div>
<div class="rowitem"><h1>{{lang "panel.forums_create_head"}}</h1></div>
</div>
<div class="colstack_item the_form">
<form action="/panel/forums/create/?session={{.CurrentUser.Session}}" method="post">
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_create_name_label"}}</a></div>
<div class="formitem"><input name="forum-name" type="text" placeholder="{{lang "panel_forums_create_name"}}" /></div>
<div class="formitem formlabel"><a>{{lang "panel.forums_create_name_label"}}</a></div>
<div class="formitem"><input name="forum-name" type="text" placeholder="{{lang "panel.forums_create_name"}}" /></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_create_description_label"}}</a></div>
<div class="formitem"><input name="forum-desc" type="text" placeholder="{{lang "panel_forums_create_description"}}" /></div>
<div class="formitem formlabel"><a>{{lang "panel.forums_create_description_label"}}</a></div>
<div class="formitem"><input name="forum-desc" type="text" placeholder="{{lang "panel.forums_create_description"}}" /></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_active_label"}}</a></div>
<div class="formitem formlabel"><a>{{lang "panel.forums_active_label"}}</a></div>
<div class="formitem"><select name="forum-active">
<option selected value="1">{{lang "option_yes"}}</option>
<option value="0">{{lang "option_no"}}</option>
</select></div>
</div>
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_forums_preset_label"}}</a></div>
<div class="formitem formlabel"><a>{{lang "panel.forums_preset_label"}}</a></div>
<div class="formitem"><select name="forum-preset">
<option selected value="all">{{lang "panel_preset_everyone"}}</option>
<option value="announce">{{lang "panel_preset_announcements"}}</option>
@ -67,7 +70,7 @@
</select></div>
</div>
<div class="formrow">
<div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel_forums_create_button"}}</button></div>
<div class="formitem"><button name="panel-button" class="formbutton">{{lang "panel.forums_create_button"}}</button></div>
</div>
</form>
</div>

View File

@ -4,20 +4,24 @@
<main class="colstack_right">
{{template "panel_before_head.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_items_head"}}</h1></div>
<div class="rowitem">
<h1>{{lang "panel_themes_menus_items_head"}}</h1>
<h2 class="hguide">{{lang "panel_hints_reorder"}}</h2>
</div>
</div>
<div id="panel_menu_item_holder" class="colstack_item rowlist">
{{range .ItemList}}
<div class="panel_menu_item rowitem panel_compactrow editable_parent" data-miid="{{.ID}}">
<span class="grip"></span>
<a href="/panel/themes/menus/item/edit/{{.ID}}" class="editable_block panel_upshift">{{.Name}}</a>
<span class="panel_buttons">
<a href="/panel/themes/menus/item/edit/{{.ID}}" class="panel_tag panel_right_button edit_button" aria-label="{{lang "panel_themes_menus_item_edit_button_aria"}}"></a>
<a href="/panel/themes/menus/item/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button delete_button" aria-label="{{lang "panel_themes_menus_item_delete_button_aria"}}"></a>
</span>
<a href="/panel/themes/menus/item/edit/{{.ID}}" class="panel_tag panel_right_button edit_button" aria-label="{{lang "panel_themes_menus_items_edit_button_aria"}}"></a>
<a href="/panel/themes/menus/item/delete/submit/{{.ID}}?session={{$.CurrentUser.Session}}" class="panel_tag panel_right_button delete_button" aria-label="{{lang "panel_themes_menus_items_delete_button_aria"}}"></a>
</span>
</div>{{end}}
</div>
<div class="colstack_item rowlist panel_submitrow">
<div class="rowitem"><button id="panel_menu_items_order_button" class="formbutton">{{lang "panel_themes_menus_edit_update_button"}}</button></div>
<div class="rowitem"><button id="panel_menu_items_order_button" class="formbutton">{{lang "panel_themes_menus_items_update_button"}}</button></div>
</div>
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_menus_create_head"}}</h1></div>
@ -80,10 +84,8 @@
// TODO: Move this into a JS file to reduce the number of possible problems
var menuItems = {};
let items = document.getElementsByClassName("panel_menu_item");
for(let i = 0; item = items[i];i++) {
let miid = item.getAttribute("data-miid");
menuItems[i] = miid;
}
for(let i = 0; item = items[i]; i++) menuItems[i] = item.getAttribute("data-miid");
Sortable.create(document.getElementById("panel_menu_item_holder"), {
sort: true,
onEnd: (evt) => {
@ -92,11 +94,12 @@ Sortable.create(document.getElementById("panel_menu_item_holder"), {
let oldMiid = menuItems[evt.newIndex];
menuItems[evt.oldIndex] = oldMiid;
let newMiid = evt.item.getAttribute("data-miid");
console.log("newMiid: ", newMiid)
console.log("newMiid: ", newMiid);
menuItems[evt.newIndex] = newMiid;
console.log("post menuItems: ", menuItems)
console.log("post menuItems: ", menuItems);
}
});
document.getElementById("panel_menu_items_order_button").addEventListener("click", () => {
let req = new XMLHttpRequest();
if(!req) {
@ -105,18 +108,13 @@ document.getElementById("panel_menu_items_order_button").addEventListener("click
}
req.onreadystatechange = () => {
try {
if(req.readyState!==XMLHttpRequest.DONE) {
return;
}
if(req.readyState!==XMLHttpRequest.DONE) return;
// TODO: Signal the error with a notice
if(req.status===200) {
let resp = JSON.parse(req.responseText);
console.log("resp: ", resp);
if(resp.success==1) {
// TODO: Have a successfully updated notice
console.log("success");
return;
}
// TODO: Have a successfully updated notice
if(resp.success==1) return;
}
} catch(ex) {
console.error("exception: ", ex)
@ -127,12 +125,8 @@ document.getElementById("panel_menu_items_order_button").addEventListener("click
req.open("POST","/panel/themes/menus/item/order/edit/submit/{{.MenuID}}?session=" + encodeURIComponent(me.User.Session));
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
let items = "";
for(let i = 0; item = menuItems[i];i++) {
items += item+",";
}
if(items.length > 0) {
items = items.slice(0,-1);
}
for(let i = 0; item = menuItems[i];i++) items += item+",";
if(items.length > 0) items = items.slice(0,-1);
req.send("js=1&amp;items={"+items+"}");
});
</script>

View File

@ -62,6 +62,12 @@
/*margin-top: -4px;*/
margin-bottom: 14px;
}
.colstack_right .colstack_head .rowitem {
display: flex;
}
.colstack_right .colstack_head h1 + h2.hguide {
margin-left: auto;
}
.footer {
margin-top: 0px;
}

View File

@ -358,6 +358,11 @@ h2 {
margin-bottom: 8px;
margin-left: 8px;
}
.rowhead h2, .colstack_head h2 {
margin-top: 0px;
margin-bottom: 0px;
margin-left: 0px;
}
.topic_create_form {
display: flex;

View File

@ -30,6 +30,17 @@ function noxMenuBind() {
}
(() => {
function moveAlerts() {
// Move the alerts above the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) $('.alert').insertBefore(colSel);
else if (colSelAlt.length > 0) $('.alert').insertBefore(colSelAlt);
else if (colSelAltAlt.length > 0) $('.alert').insertBefore(colSelAltAlt);
else $('.alert').insertAfter(".rowhead:first");
}
addInitHook("after_update_alert_list", (alertCount) => {
console.log("misc.js");
console.log("alertCount:",alertCount);
@ -57,15 +68,7 @@ function noxMenuBind() {
$(window).resize(() => noxMenuBind());
noxMenuBind();
// Move the alerts above the first header
let colSel = $(".colstack_right .colstack_head:first");
let colSelAlt = $(".colstack_right .colstack_item:first");
let colSelAltAlt = $(".colstack_right .coldyn_block:first");
if(colSel.length > 0) $('.alert').insertBefore(colSel);
else if (colSelAlt.length > 0) $('.alert').insertBefore(colSelAlt);
else if (colSelAltAlt.length > 0) $('.alert').insertBefore(colSelAltAlt);
else $('.alert').insertAfter(".rowhead:first");
moveAlerts();
$(".menu_hamburger").click(function() {
event.stopPropagation();
@ -78,4 +81,6 @@ function noxMenuBind() {
$(document).click(() => $(".more_menu").removeClass("more_menu_selected"));
});
addInitHook("after_notice", moveAlerts);
})();

View File

@ -79,6 +79,10 @@
.colstack_right .colstack_head h1 {
font-size: 21px;
}
.colstack_right .colstack_head h1 + h2.hguide {
margin-left: auto;
font-size: 17px;
}
.colstack_right .colstack_item.the_form, .colstack_right .colstack_item:not(.colstack_head):not(.rowhead) .rowitem {
background-color: #444444;
}
@ -292,6 +296,35 @@ button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .p
margin-left: 4px;
}
span.grip {
content: '....';
width: 20px;
display: inline-block;
overflow: hidden;
line-height: 5px;
padding: 3px 4px;
cursor: move;
vertical-align: middle;
margin-top: -16px;
margin-right: 12px;
font-size: 12px;
font-family: sans-serif;
letter-spacing: -3px;
color: #888888;
text-shadow: 1px 0 1px black;
margin-left: -12px;
height: 100%;
font-size: 40px;
margin-bottom: -4px;
line-height: 8px;
}
span.grip::after {
content: '... ... ... ... ... ... ...';
}
.forum_no_desc span.grip, .panel_menu_item span.grip {
height: 40px;
}
.panel_plugin_meta {
display: flex;
flex-direction: column;

View File

@ -11,6 +11,9 @@
.colstack_head .rowitem a h1 {
margin-right: 0px;
}
.rowitem h2.hguide {
font-size: 15px;
}
.rowlist .tag-mini {
font-size: 10px;

View File

@ -10,6 +10,12 @@
.submenu a {
margin-left: 8px;
}
/*.colstack_right .colstack_head .rowitem {
display: flex;
}*/
.colstack_right .colstack_head h1 + h2.hguide {
margin-left: auto;
}
.edit_button:before {
content: "{{lang "panel_edit_button_text" . }}";