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  }