github.com/kotovmak/go-admin@v1.1.1/template/template.go (about) 1 // Copyright 2019 GoAdmin Core Team. All rights reserved. 2 // Use of this source code is governed by a Apache-2.0 style 3 // license that can be found in the LICENSE file. 4 5 package template 6 7 import ( 8 "bytes" 9 "errors" 10 "html/template" 11 "path" 12 "plugin" 13 "strconv" 14 "strings" 15 "sync" 16 17 c "github.com/kotovmak/go-admin/modules/config" 18 errors2 "github.com/kotovmak/go-admin/modules/errors" 19 "github.com/kotovmak/go-admin/modules/language" 20 "github.com/kotovmak/go-admin/modules/logger" 21 "github.com/kotovmak/go-admin/modules/menu" 22 "github.com/kotovmak/go-admin/modules/system" 23 "github.com/kotovmak/go-admin/modules/utils" 24 "github.com/kotovmak/go-admin/plugins/admin/models" 25 "github.com/kotovmak/go-admin/template/login" 26 "github.com/kotovmak/go-admin/template/types" 27 ) 28 29 // Template is the interface which contains methods of ui components. 30 // It will be used in the plugins for custom the ui. 31 type Template interface { 32 Name() string 33 34 // Components 35 36 // layout 37 Col() types.ColAttribute 38 Row() types.RowAttribute 39 40 // form and table 41 Form() types.FormAttribute 42 Table() types.TableAttribute 43 DataTable() types.DataTableAttribute 44 45 TreeView() types.TreeViewAttribute 46 Tree() types.TreeAttribute 47 Tabs() types.TabsAttribute 48 Alert() types.AlertAttribute 49 Link() types.LinkAttribute 50 51 Paginator() types.PaginatorAttribute 52 Popup() types.PopupAttribute 53 Box() types.BoxAttribute 54 55 Label() types.LabelAttribute 56 Image() types.ImgAttribute 57 58 Button() types.ButtonAttribute 59 60 // Builder methods 61 GetTmplList() map[string]string 62 GetAssetList() []string 63 GetAssetImportHTML(exceptComponents ...string) template.HTML 64 GetAsset(string) ([]byte, error) 65 GetTemplate(bool) (*template.Template, string) 66 GetVersion() string 67 GetRequirements() []string 68 GetHeadHTML() template.HTML 69 GetFootJS() template.HTML 70 Get404HTML() template.HTML 71 Get500HTML() template.HTML 72 Get403HTML() template.HTML 73 } 74 75 type PageType uint8 76 77 const ( 78 NormalPage PageType = iota 79 Missing404Page 80 Error500Page 81 NoPermission403Page 82 ) 83 84 func GetPageTypeFromPageError(err errors2.PageError) PageType { 85 if err == nil { 86 return NormalPage 87 } else if err == errors2.PageError403 { 88 return NoPermission403Page 89 } else if err == errors2.PageError404 { 90 return Missing404Page 91 } else { 92 return Error500Page 93 } 94 } 95 96 const ( 97 CompCol = "col" 98 CompRow = "row" 99 CompForm = "form" 100 CompTable = "table" 101 CompDataTable = "datatable" 102 CompTree = "tree" 103 CompTreeView = "treeview" 104 CompTabs = "tabs" 105 CompAlert = "alert" 106 CompLink = "link" 107 CompPaginator = "paginator" 108 CompPopup = "popup" 109 CompBox = "box" 110 CompLabel = "label" 111 CompImage = "image" 112 CompButton = "button" 113 ) 114 115 func HTML(s string) template.HTML { 116 return template.HTML(s) 117 } 118 119 func CSS(s string) template.CSS { 120 return template.CSS(s) 121 } 122 123 func JS(s string) template.JS { 124 return template.JS(s) 125 } 126 127 // The templateMap contains templates registered. 128 var templateMap = make(map[string]Template) 129 130 // Get the template interface by theme name. If the 131 // name is not found, it panics. 132 func Get(theme string) Template { 133 if temp, ok := templateMap[theme]; ok { 134 return temp 135 } 136 panic("wrong theme name") 137 } 138 139 // Default get the default template with the theme name set with the global config. 140 // If the name is not found, it panics. 141 func Default() Template { 142 if temp, ok := templateMap[c.GetTheme()]; ok { 143 return temp 144 } 145 panic("wrong theme name") 146 } 147 148 var ( 149 templateMu sync.Mutex 150 compMu sync.Mutex 151 ) 152 153 // Add makes a template available by the provided theme name. 154 // If Add is called twice with the same name or if template is nil, 155 // it panics. 156 func Add(name string, temp Template) { 157 templateMu.Lock() 158 defer templateMu.Unlock() 159 if temp == nil { 160 panic("template is nil") 161 } 162 if _, dup := templateMap[name]; dup { 163 panic("add template twice " + name) 164 } 165 templateMap[name] = temp 166 } 167 168 // CheckRequirements check the theme and GoAdmin interdependence limit. 169 // The first return parameter means that whether GoAdmin version meets the requirement of the theme used or not. 170 // The second return parameter means that whether the version of theme used meets the requirement of GoAdmin or not. 171 func CheckRequirements() (bool, bool) { 172 if !CheckThemeRequirements() { 173 return false, true 174 } 175 // The theme which is not in the default official themes will be ignored. 176 if !utils.InArray(DefaultThemeNames, Default().Name()) { 177 return true, true 178 } 179 return true, VersionCompare(Default().GetVersion(), system.RequireThemeVersion()[Default().Name()]) 180 } 181 182 func CheckThemeRequirements() bool { 183 return VersionCompare(system.Version(), Default().GetRequirements()) 184 } 185 186 func VersionCompare(toCompare string, versions []string) bool { 187 for _, v := range versions { 188 if v == toCompare || utils.CompareVersion(v, toCompare) { 189 return true 190 } 191 } 192 return false 193 } 194 195 func GetPageContentFromPageType(title, desc, msg string, pt PageType) (template.HTML, template.HTML, template.HTML) { 196 if c.GetDebug() { 197 return template.HTML(title), template.HTML(desc), Default().Alert().SetTitle(errors2.MsgWithIcon).Warning(msg) 198 } 199 200 if pt == Missing404Page { 201 if c.GetCustom404HTML() != template.HTML("") { 202 return "", "", c.GetCustom404HTML() 203 } else { 204 return "", "", Default().Get404HTML() 205 } 206 } else if pt == NoPermission403Page { 207 if c.GetCustom404HTML() != template.HTML("") { 208 return "", "", c.GetCustom403HTML() 209 } else { 210 return "", "", Default().Get403HTML() 211 } 212 } else { 213 if c.GetCustom500HTML() != template.HTML("") { 214 return "", "", c.GetCustom500HTML() 215 } else { 216 return "", "", Default().Get500HTML() 217 } 218 } 219 } 220 221 var DefaultThemeNames = []string{"sword", "adminlte"} 222 223 func Themes() []string { 224 names := make([]string, len(templateMap)) 225 i := 0 226 for k := range templateMap { 227 names[i] = k 228 i++ 229 } 230 return names 231 } 232 233 func AddFromPlugin(name string, mod string) { 234 235 plug, err := plugin.Open(mod) 236 if err != nil { 237 logger.Error("AddFromPlugin err", err) 238 panic(err) 239 } 240 241 tempPlugin, err := plug.Lookup(strings.Title(name)) 242 if err != nil { 243 logger.Error("AddFromPlugin err", err) 244 panic(err) 245 } 246 247 var temp Template 248 temp, ok := tempPlugin.(Template) 249 if !ok { 250 logger.Error("AddFromPlugin err: unexpected type from module symbol") 251 panic(errors.New("AddFromPlugin err: unexpected type from module symbol")) 252 } 253 254 Add(name, temp) 255 } 256 257 // Component is the interface which stand for a ui component. 258 type Component interface { 259 // GetTemplate return a *template.Template and a given key. 260 GetTemplate() (*template.Template, string) 261 262 // GetAssetList return the assets url suffix used in the component. 263 // example: 264 // 265 // {{.UrlPrefix}}/assets/login/css/bootstrap.min.css => login/css/bootstrap.min.css 266 // 267 // See: 268 // https://github.com/kotovmak/go-admin/blob/master/template/login/theme1.tmpl#L32 269 // https://github.com/kotovmak/go-admin/blob/master/template/login/list.go 270 GetAssetList() []string 271 272 // GetAsset return the asset content according to the corresponding url suffix. 273 // Asset content is recommended to use the tool go-bindata to generate. 274 // 275 // See: http://github.com/jteeuwen/go-bindata 276 GetAsset(string) ([]byte, error) 277 278 GetContent() template.HTML 279 280 IsAPage() bool 281 282 GetName() string 283 284 GetJS() template.JS 285 GetCSS() template.CSS 286 GetCallbacks() types.Callbacks 287 } 288 289 var compMap = map[string]Component{ 290 "login": login.GetLoginComponent(), 291 } 292 293 // GetComp gets the component by registered name. If the 294 // name is not found, it panics. 295 func GetComp(name string) Component { 296 if comp, ok := compMap[name]; ok { 297 return comp 298 } 299 panic("wrong component name") 300 } 301 302 func GetComponentAsset() []string { 303 assets := make([]string, 0) 304 for _, comp := range compMap { 305 assets = append(assets, comp.GetAssetList()...) 306 } 307 return assets 308 } 309 310 func GetComponentAssetWithinPage() []string { 311 assets := make([]string, 0) 312 for _, comp := range compMap { 313 if !comp.IsAPage() { 314 assets = append(assets, comp.GetAssetList()...) 315 } 316 } 317 return assets 318 } 319 320 func GetComponentAssetImportHTML() (res template.HTML) { 321 res = Default().GetAssetImportHTML(c.GetExcludeThemeComponents()...) 322 assets := GetComponentAssetWithinPage() 323 for i := 0; i < len(assets); i++ { 324 res += getHTMLFromAssetUrl(assets[i]) 325 } 326 return 327 } 328 329 func getHTMLFromAssetUrl(s string) template.HTML { 330 switch path.Ext(s) { 331 case ".css": 332 return template.HTML(`<link rel="stylesheet" href="` + c.GetAssetUrl() + c.Url("/assets"+s) + `">`) 333 case ".js": 334 return template.HTML(`<script src="` + c.GetAssetUrl() + c.Url("/assets"+s) + `"></script>`) 335 default: 336 return "" 337 } 338 } 339 340 func GetAsset(path string) ([]byte, error) { 341 for _, comp := range compMap { 342 res, err := comp.GetAsset(path) 343 if err == nil { 344 return res, nil 345 } 346 } 347 return nil, errors.New(path + " not found") 348 } 349 350 // AddComp makes a component available by the provided name. 351 // If Add is called twice with the same name or if component is nil, 352 // it panics. 353 func AddComp(comp Component) { 354 compMu.Lock() 355 defer compMu.Unlock() 356 if comp == nil { 357 panic("component is nil") 358 } 359 if _, dup := compMap[comp.GetName()]; dup { 360 panic("add component twice " + comp.GetName()) 361 } 362 compMap[comp.GetName()] = comp 363 } 364 365 // AddLoginComp add the specified login component. 366 func AddLoginComp(comp Component) { 367 compMu.Lock() 368 defer compMu.Unlock() 369 compMap["login"] = comp 370 } 371 372 // SetComp makes a component available by the provided name. 373 // If the value corresponding to the key is empty or if component is nil, 374 // it panics. 375 func SetComp(name string, comp Component) { 376 compMu.Lock() 377 defer compMu.Unlock() 378 if comp == nil { 379 panic("component is nil") 380 } 381 if _, dup := compMap[name]; dup { 382 compMap[name] = comp 383 } 384 } 385 386 type ExecuteParam struct { 387 User models.UserModel 388 Tmpl *template.Template 389 TmplName string 390 IsPjax bool 391 Panel types.Panel 392 Logo template.HTML 393 Config *c.Config 394 Menu *menu.Menu 395 Animation bool 396 Buttons types.Buttons 397 NoCompress bool 398 Iframe bool 399 } 400 401 func updateNavAndLogoJS(logo template.HTML) template.JS { 402 if logo == template.HTML("") { 403 return "" 404 } 405 return `$(function () { 406 $(".logo-lg").html("` + template.JS(logo) + `"); 407 });` 408 } 409 410 func updateNavJS(isPjax bool) template.JS { 411 if !isPjax { 412 return "" 413 } 414 return `$(function () { 415 let lis = $(".navbar-custom-menu .nav.navbar-nav li"); 416 for (var i = lis.length - 8; i > -1; i--) { 417 $(lis[i]).remove(); 418 } 419 $(".navbar-custom-menu .nav.navbar-nav").prepend($("#navbar-nav-custom").html()); 420 });` 421 } 422 423 type ExecuteOptions struct { 424 Animation bool 425 NoCompress bool 426 HideSideBar bool 427 HideHeader bool 428 UpdateMenu bool 429 NavDropDownButton []*types.NavDropDownItemButton 430 } 431 432 func GetExecuteOptions(options []ExecuteOptions) ExecuteOptions { 433 if len(options) == 0 { 434 return ExecuteOptions{Animation: true} 435 } 436 return options[0] 437 } 438 439 func Execute(param *ExecuteParam) *bytes.Buffer { 440 441 buf := new(bytes.Buffer) 442 err := param.Tmpl.ExecuteTemplate(buf, param.TmplName, 443 types.NewPage(&types.NewPageParam{ 444 User: param.User, 445 Menu: param.Menu, 446 Assets: GetComponentAssetImportHTML(), 447 Buttons: param.Buttons, 448 Iframe: param.Iframe, 449 UpdateMenu: param.IsPjax, 450 Panel: param.Panel. 451 GetContent(append([]bool{param.Config.IsProductionEnvironment() && !param.NoCompress}, 452 param.Animation)...).AddJS(param.Menu.GetUpdateJS(param.IsPjax)). 453 AddJS(updateNavAndLogoJS(param.Logo)).AddJS(updateNavJS(param.IsPjax)), 454 TmplHeadHTML: Default().GetHeadHTML(), 455 TmplFootJS: Default().GetFootJS(), 456 Logo: param.Logo, 457 })) 458 if err != nil { 459 logger.Error("template execute error", err) 460 } 461 return buf 462 } 463 464 func WarningPanel(msg string, pts ...PageType) types.Panel { 465 pt := Error500Page 466 if len(pts) > 0 { 467 pt = pts[0] 468 } 469 pageTitle, description, content := GetPageContentFromPageType(msg, msg, msg, pt) 470 return types.Panel{ 471 Content: content, 472 Description: description, 473 Title: pageTitle, 474 } 475 } 476 477 func WarningPanelWithDescAndTitle(msg, desc, title string, pts ...PageType) types.Panel { 478 pt := Error500Page 479 if len(pts) > 0 { 480 pt = pts[0] 481 } 482 pageTitle, description, content := GetPageContentFromPageType(msg, desc, title, pt) 483 return types.Panel{ 484 Content: content, 485 Description: description, 486 Title: pageTitle, 487 } 488 } 489 490 var DefaultFuncMap = template.FuncMap{ 491 "lang": language.Get, 492 "langHtml": language.GetFromHtml, 493 "link": func(cdnUrl, prefixUrl, assetsUrl string) string { 494 if cdnUrl == "" { 495 return prefixUrl + assetsUrl 496 } 497 return cdnUrl + assetsUrl 498 }, 499 "isLinkUrl": func(s string) bool { 500 return (len(s) > 7 && s[:7] == "http://") || (len(s) > 8 && s[:8] == "https://") 501 }, 502 "render": func(s, old, repl template.HTML) template.HTML { 503 return template.HTML(strings.ReplaceAll(string(s), string(old), string(repl))) 504 }, 505 "renderJS": func(s template.JS, old, repl template.HTML) template.JS { 506 return template.JS(strings.ReplaceAll(string(s), string(old), string(repl))) 507 }, 508 "divide": func(a, b int) int { 509 return a / b 510 }, 511 "renderRowDataHTML": func(id, content template.HTML, value ...map[string]types.InfoItem) template.HTML { 512 return template.HTML(types.ParseTableDataTmplWithID(id, string(content), value...)) 513 }, 514 "renderRowDataJS": func(id template.HTML, content template.JS, value ...map[string]types.InfoItem) template.JS { 515 return template.JS(types.ParseTableDataTmplWithID(id, string(content), value...)) 516 }, 517 "attr": func(s template.HTML) template.HTMLAttr { 518 return template.HTMLAttr(s) 519 }, 520 "js": func(s interface{}) template.JS { 521 if ss, ok := s.(string); ok { 522 return template.JS(ss) 523 } 524 if ss, ok := s.(template.HTML); ok { 525 return template.JS(ss) 526 } 527 return "" 528 }, 529 "changeValue": func(f types.FormField, index int) types.FormField { 530 if len(f.ValueArr) > 0 { 531 f.Value = template.HTML(f.ValueArr[index]) 532 } 533 if len(f.OptionsArr) > 0 { 534 f.Options = f.OptionsArr[index] 535 } 536 if f.FormType.IsSelect() { 537 f.FieldClass += "_" + strconv.Itoa(index) 538 } 539 return f 540 }, 541 } 542 543 type BaseComponent struct { 544 Name string 545 HTMLData string 546 CSS template.CSS 547 JS template.JS 548 Callbacks types.Callbacks 549 } 550 551 func (b *BaseComponent) IsAPage() bool { return false } 552 func (b *BaseComponent) GetName() string { return b.Name } 553 func (b *BaseComponent) GetAssetList() []string { return make([]string, 0) } 554 func (b *BaseComponent) GetAsset(name string) ([]byte, error) { return nil, nil } 555 func (b *BaseComponent) GetJS() template.JS { return b.JS } 556 func (b *BaseComponent) GetCSS() template.CSS { return b.CSS } 557 func (b *BaseComponent) GetCallbacks() types.Callbacks { return b.Callbacks } 558 func (b *BaseComponent) BindActionTo(action types.Action, id string) { 559 action.SetBtnId(id) 560 b.JS += action.Js() 561 b.HTMLData += string(action.ExtContent()) 562 b.Callbacks = append(b.Callbacks, action.GetCallbacks()) 563 } 564 func (b *BaseComponent) GetContentWithData(obj interface{}) template.HTML { 565 buffer := new(bytes.Buffer) 566 tmpl, defineName := b.GetTemplate() 567 err := tmpl.ExecuteTemplate(buffer, defineName, obj) 568 if err != nil { 569 logger.Error(b.Name+" GetContent error:", err) 570 } 571 return template.HTML(buffer.String()) 572 } 573 574 func (b *BaseComponent) GetTemplate() (*template.Template, string) { 575 tmpl, err := template.New(b.Name). 576 Funcs(DefaultFuncMap). 577 Parse(b.HTMLData) 578 579 if err != nil { 580 logger.Error(b.Name+" GetTemplate Error: ", err) 581 } 582 583 return tmpl, b.Name 584 }