init
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/version"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/web"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
utils "github.com/analogj/go-util/utils"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var goos string
|
||||
var goarch string
|
||||
|
||||
func main() {
|
||||
|
||||
config, err := config.Create()
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
//we're going to load the config file manually, since we need to validate it.
|
||||
err = config.ReadConfig("/scrutiny/config/scrutiny.yaml") // Find and read the config file
|
||||
if _, ok := err.(errors.ConfigFileMissingError); ok { // Handle errors reading the config file
|
||||
//ignore "could not find config file"
|
||||
} else if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cli.CommandHelpTemplate = `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
USAGE:
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}}
|
||||
CATEGORY:
|
||||
{{.Category}}{{end}}{{if .Description}}
|
||||
DESCRIPTION:
|
||||
{{.Description}}{{end}}{{if .VisibleFlags}}
|
||||
OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
`
|
||||
|
||||
app := &cli.App{
|
||||
Name: "scrutiny",
|
||||
Usage: "WebUI for smartd S.M.A.R.T monitoring",
|
||||
Version: version.VERSION,
|
||||
Compiled: time.Now(),
|
||||
Authors: []*cli.Author{
|
||||
{
|
||||
Name: "Jason Kulatunga",
|
||||
Email: "jason@thesparktree.com",
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
|
||||
drawbridge := "github.com/AnalogJ/scrutiny"
|
||||
|
||||
var versionInfo string
|
||||
if len(goos) > 0 && len(goarch) > 0 {
|
||||
versionInfo = fmt.Sprintf("%s.%s-%s", goos, goarch, version.VERSION)
|
||||
} else {
|
||||
versionInfo = fmt.Sprintf("dev-%s", version.VERSION)
|
||||
}
|
||||
|
||||
subtitle := drawbridge + utils.LeftPad2Len(versionInfo, " ", 65-len(drawbridge))
|
||||
|
||||
color.New(color.FgGreen).Fprintf(c.App.Writer, fmt.Sprintf(utils.StripIndent(
|
||||
`
|
||||
___ ___ ____ __ __ ____ ____ _ _ _ _
|
||||
/ __) / __)( _ \( )( )(_ _)(_ _)( \( )( \/ )
|
||||
\__ \( (__ ) / )(__)( )( _)(_ ) ( \ /
|
||||
(___/ \___)(_)\_)(______) (__) (____)(_)\_) (__)
|
||||
%s
|
||||
|
||||
`), subtitle))
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "start",
|
||||
Usage: "Start the scrutiny server",
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Fprintln(c.App.Writer, c.Command.Usage)
|
||||
if c.IsSet("config") {
|
||||
err = config.ReadConfig(c.String("config")) // Find and read the config file
|
||||
if err != nil { // Handle errors reading the config file
|
||||
//ignore "could not find config file"
|
||||
fmt.Printf("Could not find config file at specified path: %s", c.String("config"))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
webServer := web.AppEngine{Config: config}
|
||||
|
||||
return webServer.Start()
|
||||
},
|
||||
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Usage: "Specify the path to the config file",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(color.HiRedString("ERROR: %v", err))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/analogj/go-util/utils"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// When initializing this class the following methods must be called:
|
||||
// Config.New
|
||||
// Config.Init
|
||||
// This is done automatically when created via the Factory.
|
||||
type configuration struct {
|
||||
*viper.Viper
|
||||
}
|
||||
|
||||
//Viper uses the following precedence order. Each item takes precedence over the item below it:
|
||||
// explicit call to Set
|
||||
// flag
|
||||
// env
|
||||
// config
|
||||
// key/value store
|
||||
// default
|
||||
|
||||
func (c *configuration) Init() error {
|
||||
c.Viper = viper.New()
|
||||
//set defaults
|
||||
c.SetDefault("web.listen.port", "8080")
|
||||
c.SetDefault("web.listen.host", "0.0.0.0")
|
||||
c.SetDefault("web.src.frontend.path", "/scrutiny/web")
|
||||
|
||||
c.SetDefault("web.database.location", "/scrutiny/config/scrutiny.db")
|
||||
|
||||
c.SetDefault("disks.include", []string{})
|
||||
c.SetDefault("disks.exclude", []string{})
|
||||
|
||||
c.SetDefault("notify.metric.script", "/scrutiny/config/notify-metrics.sh")
|
||||
c.SetDefault("notify.long.script", "/scrutiny/config/notify-long-test.sh")
|
||||
c.SetDefault("notify.short.script", "/scrutiny/config/notify-short-test.sh")
|
||||
|
||||
c.SetDefault("collect.metric.enable", true)
|
||||
c.SetDefault("collect.metric.command", "-a -o on -S on")
|
||||
c.SetDefault("collect.long.enable", true)
|
||||
c.SetDefault("collect.long.command", "-a -o on -S on")
|
||||
c.SetDefault("collect.short.enable", true)
|
||||
c.SetDefault("collect.short.command", "-a -o on -S on")
|
||||
|
||||
//if you want to load a non-standard location system config file (~/drawbridge.yml), use ReadConfig
|
||||
c.SetConfigType("yaml")
|
||||
//c.SetConfigName("drawbridge")
|
||||
//c.AddConfigPath("$HOME/")
|
||||
|
||||
//CLI options will be added via the `Set()` function
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *configuration) ReadConfig(configFilePath string) error {
|
||||
configFilePath, err := utils.ExpandPath(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !utils.FileExists(configFilePath) {
|
||||
log.Printf("No configuration file found at %v. Skipping", configFilePath)
|
||||
return errors.ConfigFileMissingError("The configuration file could not be found.")
|
||||
}
|
||||
|
||||
//validate config file contents
|
||||
//err = c.ValidateConfigFile(configFilePath)
|
||||
//if err != nil {
|
||||
// log.Printf("Config file at `%v` is invalid: %s", configFilePath, err)
|
||||
// return err
|
||||
//}
|
||||
|
||||
log.Printf("Loading configuration file: %s", configFilePath)
|
||||
|
||||
config_data, err := os.Open(configFilePath)
|
||||
if err != nil {
|
||||
log.Printf("Error reading configuration file: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.MergeConfig(config_data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.ValidateConfig()
|
||||
}
|
||||
|
||||
// This function ensures that the merged config works correctly.
|
||||
func (c *configuration) ValidateConfig() error {
|
||||
|
||||
////deserialize Questions
|
||||
//questionsMap := map[string]Question{}
|
||||
//err := c.UnmarshalKey("questions", &questionsMap)
|
||||
//
|
||||
//if err != nil {
|
||||
// log.Printf("questions could not be deserialized correctly. %v", err)
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//for _, v := range questionsMap {
|
||||
//
|
||||
// typeContent, ok := v.Schema["type"].(string)
|
||||
// if !ok || len(typeContent) == 0 {
|
||||
// return errors.QuestionSyntaxError("`type` is required for questions")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
func Create() (Interface, error) {
|
||||
config := new(configuration)
|
||||
if err := config.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Create mock using:
|
||||
// mockgen -source=pkg/config/interface.go -destination=pkg/config/mock/mock_config.go
|
||||
type Interface interface {
|
||||
Init() error
|
||||
ReadConfig(configFilePath string) error
|
||||
Set(key string, value interface{})
|
||||
SetDefault(key string, value interface{})
|
||||
|
||||
AllSettings() map[string]interface{}
|
||||
IsSet(key string) bool
|
||||
Get(key string) interface{}
|
||||
GetBool(key string) bool
|
||||
GetInt(key string) int
|
||||
GetString(key string) string
|
||||
GetStringSlice(key string) []string
|
||||
UnmarshalKey(key string, rawVal interface{}, decoderOpts ...viper.DecoderConfigOption) error
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
)
|
||||
|
||||
func DatabaseHandler(dbPath string) gin.HandlerFunc {
|
||||
//var database *gorm.DB
|
||||
fmt.Printf("Trying to connect to database stored: %s", dbPath)
|
||||
database, err := gorm.Open("sqlite3", dbPath)
|
||||
|
||||
if err != nil {
|
||||
panic("Failed to connect to database!")
|
||||
}
|
||||
|
||||
database.AutoMigrate(&db.Device{})
|
||||
database.AutoMigrate(&db.SelfTest{})
|
||||
database.AutoMigrate(&db.Smart{})
|
||||
database.AutoMigrate(&db.SmartAttribute{})
|
||||
|
||||
//TODO: detrmine where we can call defer database.Close()
|
||||
return func(c *gin.Context) {
|
||||
c.Set("DB", database)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Raised when config file is missing
|
||||
type ConfigFileMissingError string
|
||||
|
||||
func (str ConfigFileMissingError) Error() string {
|
||||
return fmt.Sprintf("ConfigFileMissingError: %q", string(str))
|
||||
}
|
||||
|
||||
// Raised when the config file doesnt match schema
|
||||
type ConfigValidationError string
|
||||
|
||||
func (str ConfigValidationError) Error() string {
|
||||
return fmt.Sprintf("ConfigValidationError: %q", string(str))
|
||||
}
|
||||
|
||||
// Raised when a dependency (like smartd or ssh-agent) is missing
|
||||
type DependencyMissingError string
|
||||
|
||||
func (str DependencyMissingError) Error() string {
|
||||
return fmt.Sprintf("DependencyMissingError: %q", string(str))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//func TestCheckErr_WithoutError(t *testing.T) {
|
||||
// t.Parallel()
|
||||
//
|
||||
// //assert
|
||||
// require.NotPanics(t, func() {
|
||||
// errors.CheckErr(nil)
|
||||
// })
|
||||
//}
|
||||
|
||||
//func TestCheckErr_Error(t *testing.T) {
|
||||
// t.Parallel()
|
||||
//
|
||||
// //assert
|
||||
// require.Panics(t, func() {
|
||||
// errors.CheckErr(stderrors.New("This is an error"))
|
||||
// })
|
||||
//}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//assert
|
||||
require.Implements(t, (*error)(nil), errors.ConfigFileMissingError("test"), "should implement the error interface")
|
||||
require.Implements(t, (*error)(nil), errors.ConfigValidationError("test"), "should implement the error interface")
|
||||
require.Implements(t, (*error)(nil), errors.DependencyMissingError("test"), "should implement the error interface")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
package collector
|
||||
|
||||
type SmartInfo struct {
|
||||
JSONFormatVersion []int `json:"json_format_version"`
|
||||
Smartctl struct {
|
||||
Version []int `json:"version"`
|
||||
SvnRevision string `json:"svn_revision"`
|
||||
PlatformInfo string `json:"platform_info"`
|
||||
BuildInfo string `json:"build_info"`
|
||||
Argv []string `json:"argv"`
|
||||
ExitStatus int `json:"exit_status"`
|
||||
Messages []struct {
|
||||
String string `json:"string"`
|
||||
Severity string `json:"severity"`
|
||||
} `json:"messages"`
|
||||
} `json:"smartctl"`
|
||||
Device struct {
|
||||
Name string `json:"name"`
|
||||
InfoName string `json:"info_name"`
|
||||
Type string `json:"type"`
|
||||
Protocol string `json:"protocol"`
|
||||
} `json:"device"`
|
||||
ModelName string `json:"model_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Wwn struct {
|
||||
Naa int `json:"naa"`
|
||||
Oui int `json:"oui"`
|
||||
ID int64 `json:"id"`
|
||||
} `json:"wwn"`
|
||||
FirmwareVersion string `json:"firmware_version"`
|
||||
UserCapacity struct {
|
||||
Blocks int64 `json:"blocks"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
} `json:"user_capacity"`
|
||||
LogicalBlockSize int `json:"logical_block_size"`
|
||||
PhysicalBlockSize int `json:"physical_block_size"`
|
||||
RotationRate int `json:"rotation_rate"`
|
||||
FormFactor struct {
|
||||
AtaValue int `json:"ata_value"`
|
||||
Name string `json:"name"`
|
||||
} `json:"form_factor"`
|
||||
InSmartctlDatabase bool `json:"in_smartctl_database"`
|
||||
AtaVersion struct {
|
||||
String string `json:"string"`
|
||||
MajorValue int `json:"major_value"`
|
||||
MinorValue int `json:"minor_value"`
|
||||
} `json:"ata_version"`
|
||||
SataVersion struct {
|
||||
String string `json:"string"`
|
||||
Value int `json:"value"`
|
||||
} `json:"sata_version"`
|
||||
InterfaceSpeed struct {
|
||||
Max struct {
|
||||
SataValue int `json:"sata_value"`
|
||||
String string `json:"string"`
|
||||
UnitsPerSecond int `json:"units_per_second"`
|
||||
BitsPerUnit int `json:"bits_per_unit"`
|
||||
} `json:"max"`
|
||||
Current struct {
|
||||
SataValue int `json:"sata_value"`
|
||||
String string `json:"string"`
|
||||
UnitsPerSecond int `json:"units_per_second"`
|
||||
BitsPerUnit int `json:"bits_per_unit"`
|
||||
} `json:"current"`
|
||||
} `json:"interface_speed"`
|
||||
LocalTime struct {
|
||||
TimeT int64 `json:"time_t"`
|
||||
Asctime string `json:"asctime"`
|
||||
} `json:"local_time"`
|
||||
SmartStatus struct {
|
||||
Passed bool `json:"passed"`
|
||||
} `json:"smart_status"`
|
||||
AtaSmartData struct {
|
||||
OfflineDataCollection struct {
|
||||
Status struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
Passed bool `json:"passed"`
|
||||
} `json:"status"`
|
||||
CompletionSeconds int `json:"completion_seconds"`
|
||||
} `json:"offline_data_collection"`
|
||||
SelfTest struct {
|
||||
Status struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
RemainingPercent int `json:"remaining_percent"`
|
||||
} `json:"status"`
|
||||
PollingMinutes struct {
|
||||
Short int `json:"short"`
|
||||
Extended int `json:"extended"`
|
||||
} `json:"polling_minutes"`
|
||||
} `json:"self_test"`
|
||||
Capabilities struct {
|
||||
Values []int `json:"values"`
|
||||
ExecOfflineImmediateSupported bool `json:"exec_offline_immediate_supported"`
|
||||
OfflineIsAbortedUponNewCmd bool `json:"offline_is_aborted_upon_new_cmd"`
|
||||
OfflineSurfaceScanSupported bool `json:"offline_surface_scan_supported"`
|
||||
SelfTestsSupported bool `json:"self_tests_supported"`
|
||||
ConveyanceSelfTestSupported bool `json:"conveyance_self_test_supported"`
|
||||
SelectiveSelfTestSupported bool `json:"selective_self_test_supported"`
|
||||
AttributeAutosaveEnabled bool `json:"attribute_autosave_enabled"`
|
||||
ErrorLoggingSupported bool `json:"error_logging_supported"`
|
||||
GpLoggingSupported bool `json:"gp_logging_supported"`
|
||||
} `json:"capabilities"`
|
||||
} `json:"ata_smart_data"`
|
||||
AtaSctCapabilities struct {
|
||||
Value int `json:"value"`
|
||||
ErrorRecoveryControlSupported bool `json:"error_recovery_control_supported"`
|
||||
FeatureControlSupported bool `json:"feature_control_supported"`
|
||||
DataTableSupported bool `json:"data_table_supported"`
|
||||
} `json:"ata_sct_capabilities"`
|
||||
AtaSmartAttributes struct {
|
||||
Revision int `json:"revision"`
|
||||
Table []struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value int `json:"value"`
|
||||
Worst int `json:"worst"`
|
||||
Thresh int `json:"thresh"`
|
||||
WhenFailed string `json:"when_failed"`
|
||||
Flags struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
Prefailure bool `json:"prefailure"`
|
||||
UpdatedOnline bool `json:"updated_online"`
|
||||
Performance bool `json:"performance"`
|
||||
ErrorRate bool `json:"error_rate"`
|
||||
EventCount bool `json:"event_count"`
|
||||
AutoKeep bool `json:"auto_keep"`
|
||||
} `json:"flags"`
|
||||
Raw struct {
|
||||
Value int64 `json:"value"`
|
||||
String string `json:"string"`
|
||||
} `json:"raw"`
|
||||
} `json:"table"`
|
||||
} `json:"ata_smart_attributes"`
|
||||
PowerOnTime struct {
|
||||
Hours int64 `json:"hours"`
|
||||
} `json:"power_on_time"`
|
||||
PowerCycleCount int64 `json:"power_cycle_count"`
|
||||
Temperature struct {
|
||||
Current int64 `json:"current"`
|
||||
} `json:"temperature"`
|
||||
AtaSmartErrorLog struct {
|
||||
Summary struct {
|
||||
Revision int `json:"revision"`
|
||||
Count int `json:"count"`
|
||||
} `json:"summary"`
|
||||
} `json:"ata_smart_error_log"`
|
||||
AtaSmartSelfTestLog struct {
|
||||
Standard struct {
|
||||
Revision int `json:"revision"`
|
||||
Table []struct {
|
||||
Type struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
} `json:"type"`
|
||||
Status struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
Passed bool `json:"passed"`
|
||||
} `json:"status"`
|
||||
LifetimeHours int `json:"lifetime_hours"`
|
||||
} `json:"table"`
|
||||
Count int `json:"count"`
|
||||
ErrorCountTotal int `json:"error_count_total"`
|
||||
ErrorCountOutdated int `json:"error_count_outdated"`
|
||||
} `json:"standard"`
|
||||
} `json:"ata_smart_self_test_log"`
|
||||
AtaSmartSelectiveSelfTestLog struct {
|
||||
Revision int `json:"revision"`
|
||||
Table []struct {
|
||||
LbaMin int `json:"lba_min"`
|
||||
LbaMax int `json:"lba_max"`
|
||||
Status struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
} `json:"status"`
|
||||
} `json:"table"`
|
||||
Flags struct {
|
||||
Value int `json:"value"`
|
||||
RemainderScanEnabled bool `json:"remainder_scan_enabled"`
|
||||
} `json:"flags"`
|
||||
PowerUpScanResumeMinutes int `json:"power_up_scan_resume_minutes"`
|
||||
} `json:"ata_smart_selective_self_test_log"`
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeviceRespWrapper struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []error `json:"errors"`
|
||||
Data []Device `json:"data"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
WWN string `json:"wwn" gorm:"primary_key"`
|
||||
|
||||
DeviceName string `json:"device_name"`
|
||||
Manufacturer string `json:"manufacturer"`
|
||||
ModelName string `json:"model_name"`
|
||||
InterfaceType string `json:"interface_type"`
|
||||
InterfaceSpeed string `json:"interface_speed"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Firmware string `json:"firmware"`
|
||||
RotationSpeed int `json:"rotational_speed"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
FormFactor string `json:"form_factor"`
|
||||
SmartSupport bool `json:"smart_support"`
|
||||
|
||||
SmartResults []Smart `gorm:"foreignkey:DeviceWWN" json:"smart_results"`
|
||||
}
|
||||
|
||||
//This method requires a device with an array of SmartResults.
|
||||
//It will remove all SmartResults other than the first (the latest one)
|
||||
//All removed SmartResults, will be processed, grouping SmartAttribute by attribute_id
|
||||
// and adding theme to an array called History.
|
||||
func (dv *Device) SquashHistory() error {
|
||||
if len(dv.SmartResults) <= 1 {
|
||||
return nil //no history found. ignore
|
||||
}
|
||||
|
||||
latestSmartResultSlice := dv.SmartResults[0:1]
|
||||
historicalSmartResultSlice := dv.SmartResults[1:]
|
||||
|
||||
//re-assign the latest slice to the SmartResults field
|
||||
dv.SmartResults = latestSmartResultSlice
|
||||
|
||||
//process the historical slice
|
||||
history := map[int][]SmartAttribute{}
|
||||
for _, smartResult := range historicalSmartResultSlice {
|
||||
for _, smartAttribute := range smartResult.SmartAttributes {
|
||||
if _, ok := history[smartAttribute.AttributeId]; !ok {
|
||||
history[smartAttribute.AttributeId] = []SmartAttribute{}
|
||||
}
|
||||
history[smartAttribute.AttributeId] = append(history[smartAttribute.AttributeId], smartAttribute)
|
||||
}
|
||||
}
|
||||
|
||||
//now assign the historical slices to the SmartAttributes in the latest SmartResults
|
||||
for sandx, smartAttribute := range dv.SmartResults[0].SmartAttributes {
|
||||
if attributeHistory, ok := history[smartAttribute.AttributeId]; ok {
|
||||
dv.SmartResults[0].SmartAttributes[sandx].History = attributeHistory
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dv *Device) ApplyMetadataRules() error {
|
||||
//embed metadata in the latest smart attributes object
|
||||
if len(dv.SmartResults) > 0 {
|
||||
for ndx, attr := range dv.SmartResults[0].SmartAttributes {
|
||||
if strings.ToUpper(attr.WhenFailed) == SmartWhenFailedFailingNow {
|
||||
//this attribute has previously failed
|
||||
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusFailed
|
||||
dv.SmartResults[0].SmartAttributes[ndx].StatusReason = "Attribute is failing manufacturer SMART threshold"
|
||||
|
||||
} else if strings.ToUpper(attr.WhenFailed) == SmartWhenFailedInThePast {
|
||||
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusWarning
|
||||
dv.SmartResults[0].SmartAttributes[ndx].StatusReason = "Attribute has previously failed manufacturer SMART threshold"
|
||||
}
|
||||
|
||||
if smartMetadata, ok := metadata.AtaSmartAttributes[attr.AttributeId]; ok {
|
||||
dv.SmartResults[0].SmartAttributes[ndx].MetadataObservedThresholdStatus(smartMetadata)
|
||||
}
|
||||
|
||||
//check if status is blank, set to "passed"
|
||||
if len(dv.SmartResults[0].SmartAttributes[ndx].Status) == 0 {
|
||||
dv.SmartResults[0].SmartAttributes[ndx].Status = SmartAttributeStatusPassed
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dv *Device) UpdateFromCollectorSmartInfo(info collector.SmartInfo) error {
|
||||
dv.InterfaceSpeed = info.InterfaceSpeed.Current.String
|
||||
dv.Firmware = info.FirmwareVersion
|
||||
dv.RotationSpeed = info.RotationRate
|
||||
dv.Capacity = info.UserCapacity.Bytes
|
||||
dv.FormFactor = info.FormFactor.Name
|
||||
//dv.SmartSupport =
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package db
|
||||
|
||||
import "time"
|
||||
|
||||
type SelfTest struct {
|
||||
//GORM attributes, see: http://gorm.io/docs/conventions.html
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
DeviceWWN string
|
||||
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
|
||||
Date time.Time
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/jinzhu/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SmartWhenFailedFailingNow = "FAILING_NOW"
|
||||
const SmartWhenFailedInThePast = "IN_THE_PAST"
|
||||
|
||||
type Smart struct {
|
||||
gorm.Model
|
||||
|
||||
DeviceWWN string `json:"device_wwn"`
|
||||
Device Device `json:"-" gorm:"foreignkey:DeviceWWN"` // use DeviceWWN as foreign key
|
||||
|
||||
TestDate time.Time `json:"date"`
|
||||
SmartStatus string `json:"smart_status"`
|
||||
|
||||
//Metrics
|
||||
Temp int64 `json:"temp"`
|
||||
PowerOnHours int64 `json:"power_on_hours"`
|
||||
PowerCycleCount int64 `json:"power_cycle_count"`
|
||||
|
||||
SmartAttributes []SmartAttribute `json:"smart_attributes" gorm:"foreignkey:SmartId"`
|
||||
}
|
||||
|
||||
func (sm *Smart) FromCollectorSmartInfo(wwn string, info collector.SmartInfo) error {
|
||||
sm.DeviceWWN = wwn
|
||||
sm.TestDate = time.Unix(info.LocalTime.TimeT, 0)
|
||||
|
||||
//smart metrics
|
||||
sm.Temp = info.Temperature.Current
|
||||
sm.PowerCycleCount = info.PowerCycleCount
|
||||
sm.PowerOnHours = info.PowerOnTime.Hours
|
||||
|
||||
sm.SmartAttributes = []SmartAttribute{}
|
||||
for _, collectorAttr := range info.AtaSmartAttributes.Table {
|
||||
attrModel := SmartAttribute{
|
||||
AttributeId: collectorAttr.ID,
|
||||
Name: collectorAttr.Name,
|
||||
Value: collectorAttr.Value,
|
||||
Worst: collectorAttr.Worst,
|
||||
Threshold: collectorAttr.Thresh,
|
||||
RawValue: collectorAttr.Raw.Value,
|
||||
RawString: collectorAttr.Raw.String,
|
||||
WhenFailed: collectorAttr.WhenFailed,
|
||||
}
|
||||
|
||||
//now that we've parsed the data from the smartctl response, lets match it against our metadata rules and add additional Scrutiny specific data.
|
||||
if smartMetadata, ok := metadata.AtaSmartAttributes[collectorAttr.ID]; ok {
|
||||
attrModel.Name = smartMetadata.DisplayName
|
||||
if smartMetadata.Transform != nil {
|
||||
attrModel.TransformedValue = smartMetadata.Transform(attrModel.Value, attrModel.RawValue, attrModel.RawString)
|
||||
}
|
||||
}
|
||||
sm.SmartAttributes = append(sm.SmartAttributes, attrModel)
|
||||
}
|
||||
|
||||
if info.SmartStatus.Passed {
|
||||
sm.SmartStatus = "passed"
|
||||
} else {
|
||||
sm.SmartStatus = "failed"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const SmartAttributeStatusPassed = "passed"
|
||||
const SmartAttributeStatusFailed = "failed"
|
||||
const SmartAttributeStatusWarning = "warn"
|
||||
|
||||
type SmartAttribute struct {
|
||||
gorm.Model
|
||||
|
||||
SmartId int `json:"smart_id"`
|
||||
Smart Device `json:"-" gorm:"foreignkey:SmartId"` // use SmartId as foreign key
|
||||
|
||||
AttributeId int `json:"attribute_id"`
|
||||
Name string `json:"name"`
|
||||
Value int `json:"value"`
|
||||
Worst int `json:"worst"`
|
||||
Threshold int `json:"thresh"`
|
||||
RawValue int64 `json:"raw_value"`
|
||||
RawString string `json:"raw_string"`
|
||||
WhenFailed string `json:"when_failed"`
|
||||
|
||||
TransformedValue int64 `json:"transformed_value"`
|
||||
Status string `gorm:"-" json:"status,omitempty"`
|
||||
StatusReason string `gorm:"-" json:"status_reason,omitempty"`
|
||||
FailureRate float64 `gorm:"-" json:"failure_rate,omitempty"`
|
||||
History []SmartAttribute `gorm:"-" json:"history,omitempty"`
|
||||
}
|
||||
|
||||
// compare the attribute (raw, normalized, transformed) value to observed thresholds, and update status if necessary
|
||||
func (sa *SmartAttribute) MetadataObservedThresholdStatus(smartMetadata metadata.AtaSmartAttribute) {
|
||||
//TODO: multiple rules
|
||||
// try to predict the failure rates for observed thresholds that have 0 failure rate and error bars.
|
||||
// - if the attribute is critical
|
||||
// - the failure rate is over 10 - set to failed
|
||||
// - the attribute does not match any threshold, set to warn
|
||||
// - if the attribute is not critical
|
||||
// - if failure rate is above 20 - set to failed
|
||||
// - if failure rate is above 10 but below 20 - set to warn
|
||||
|
||||
//update the smart attribute status based on Observed thresholds.
|
||||
var value int64
|
||||
if smartMetadata.DisplayType == metadata.AtaSmartAttributeDisplayTypeNormalized {
|
||||
value = int64(sa.Value)
|
||||
} else if smartMetadata.DisplayType == metadata.AtaSmartAttributeDisplayTypeTransformed {
|
||||
value = sa.TransformedValue
|
||||
} else {
|
||||
value = sa.RawValue
|
||||
}
|
||||
|
||||
for _, obsThresh := range smartMetadata.ObservedThresholds {
|
||||
|
||||
//check if "value" is in this bucket
|
||||
if ((obsThresh.Low == obsThresh.High) && value == obsThresh.Low) ||
|
||||
(obsThresh.Low < value && value <= obsThresh.High) {
|
||||
sa.FailureRate = obsThresh.AnnualFailureRate
|
||||
|
||||
if smartMetadata.Critical {
|
||||
if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Critical Attribute is greater than 10%"
|
||||
}
|
||||
} else {
|
||||
if obsThresh.AnnualFailureRate >= 0.20 {
|
||||
sa.Status = SmartAttributeStatusFailed
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 20%"
|
||||
} else if obsThresh.AnnualFailureRate >= 0.10 {
|
||||
sa.Status = SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Observed Failure Rate for Attribute is greater than 10%"
|
||||
}
|
||||
}
|
||||
|
||||
//we've found the correct bucket, we can drop out of this loop
|
||||
return
|
||||
}
|
||||
}
|
||||
// no bucket found
|
||||
if smartMetadata.Critical {
|
||||
sa.Status = SmartAttributeStatusWarning
|
||||
sa.StatusReason = "Could not determine Observed Failure Rate for Critical Attribute"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFromCollectorSmartInfo(t *testing.T) {
|
||||
//setup
|
||||
smartDataFile, err := os.Open("../testdata/smart.json")
|
||||
require.NoError(t, err)
|
||||
defer smartDataFile.Close()
|
||||
|
||||
var smartJson collector.SmartInfo
|
||||
|
||||
smartDataBytes, err := ioutil.ReadAll(smartDataFile)
|
||||
require.NoError(t, err)
|
||||
err = json.Unmarshal(smartDataBytes, &smartJson)
|
||||
require.NoError(t, err)
|
||||
|
||||
//test
|
||||
smartMdl := db.Smart{}
|
||||
err = smartMdl.FromCollectorSmartInfo("WWN-test", smartJson)
|
||||
|
||||
//assert
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, smartMdl.DeviceWWN, "WWN-test")
|
||||
require.Equal(t, smartMdl.SmartStatus, "PASSED")
|
||||
|
||||
//check that temperature was correctly parsed
|
||||
for _, attr := range smartMdl.SmartAttributes {
|
||||
if attr.AttributeId == 194 {
|
||||
require.Equal(t, int64(163210330144), attr.RawValue)
|
||||
require.Equal(t, int64(32), attr.TransformedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
+846
@@ -0,0 +1,846 @@
|
||||
{
|
||||
"json_format_version": [
|
||||
1,
|
||||
0
|
||||
],
|
||||
"smartctl": {
|
||||
"version": [
|
||||
7,
|
||||
0
|
||||
],
|
||||
"svn_revision": "4883",
|
||||
"platform_info": "x86_64-linux-4.19.128-flatcar",
|
||||
"build_info": "(local build)",
|
||||
"argv": [
|
||||
"smartctl",
|
||||
"-j",
|
||||
"-a",
|
||||
"/dev/sdb"
|
||||
],
|
||||
"exit_status": 0
|
||||
},
|
||||
"device": {
|
||||
"name": "/dev/sdb",
|
||||
"info_name": "/dev/sdb [SAT]",
|
||||
"type": "sat",
|
||||
"protocol": "ATA"
|
||||
},
|
||||
"model_name": "WDC WD140EDFZ-11A0VA0",
|
||||
"serial_number": "9RK1XXXX",
|
||||
"wwn": {
|
||||
"naa": 5,
|
||||
"oui": 3274,
|
||||
"id": 10283057623
|
||||
},
|
||||
"firmware_version": "81.00A81",
|
||||
"user_capacity": {
|
||||
"blocks": 27344764928,
|
||||
"bytes": 14000519643136
|
||||
},
|
||||
"logical_block_size": 512,
|
||||
"physical_block_size": 4096,
|
||||
"rotation_rate": 5400,
|
||||
"form_factor": {
|
||||
"ata_value": 2,
|
||||
"name": "3.5 inches"
|
||||
},
|
||||
"in_smartctl_database": false,
|
||||
"ata_version": {
|
||||
"string": "ACS-2, ATA8-ACS T13/1699-D revision 4",
|
||||
"major_value": 1020,
|
||||
"minor_value": 41
|
||||
},
|
||||
"sata_version": {
|
||||
"string": "SATA 3.2",
|
||||
"value": 255
|
||||
},
|
||||
"interface_speed": {
|
||||
"max": {
|
||||
"sata_value": 14,
|
||||
"string": "6.0 Gb/s",
|
||||
"units_per_second": 60,
|
||||
"bits_per_unit": 100000000
|
||||
},
|
||||
"current": {
|
||||
"sata_value": 3,
|
||||
"string": "6.0 Gb/s",
|
||||
"units_per_second": 60,
|
||||
"bits_per_unit": 100000000
|
||||
}
|
||||
},
|
||||
"local_time": {
|
||||
"time_t": 1592697810,
|
||||
"asctime": "Sun Jun 21 00:03:30 2020 UTC"
|
||||
},
|
||||
"smart_status": {
|
||||
"passed": true
|
||||
},
|
||||
"ata_smart_data": {
|
||||
"offline_data_collection": {
|
||||
"status": {
|
||||
"value": 130,
|
||||
"string": "was completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"completion_seconds": 101
|
||||
},
|
||||
"self_test": {
|
||||
"status": {
|
||||
"value": 241,
|
||||
"string": "in progress, 10% remaining",
|
||||
"remaining_percent": 10
|
||||
},
|
||||
"polling_minutes": {
|
||||
"short": 2,
|
||||
"extended": 1479
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"values": [
|
||||
91,
|
||||
3
|
||||
],
|
||||
"exec_offline_immediate_supported": true,
|
||||
"offline_is_aborted_upon_new_cmd": false,
|
||||
"offline_surface_scan_supported": true,
|
||||
"self_tests_supported": true,
|
||||
"conveyance_self_test_supported": false,
|
||||
"selective_self_test_supported": true,
|
||||
"attribute_autosave_enabled": true,
|
||||
"error_logging_supported": true,
|
||||
"gp_logging_supported": true
|
||||
}
|
||||
},
|
||||
"ata_sct_capabilities": {
|
||||
"value": 61,
|
||||
"error_recovery_control_supported": true,
|
||||
"feature_control_supported": true,
|
||||
"data_table_supported": true
|
||||
},
|
||||
"ata_smart_attributes": {
|
||||
"revision": 16,
|
||||
"table": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Raw_Read_Error_Rate",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 1,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 11,
|
||||
"string": "PO-R-- ",
|
||||
"prefailure": true,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": true,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Throughput_Performance",
|
||||
"value": 135,
|
||||
"worst": 135,
|
||||
"thresh": 54,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 4,
|
||||
"string": "--S--- ",
|
||||
"prefailure": false,
|
||||
"updated_online": false,
|
||||
"performance": true,
|
||||
"error_rate": false,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 108,
|
||||
"string": "108"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Spin_Up_Time",
|
||||
"value": 81,
|
||||
"worst": 81,
|
||||
"thresh": 1,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 7,
|
||||
"string": "POS--- ",
|
||||
"prefailure": true,
|
||||
"updated_online": true,
|
||||
"performance": true,
|
||||
"error_rate": false,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 30089675132,
|
||||
"string": "380 (Average 380)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Start_Stop_Count",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 18,
|
||||
"string": "-O--C- ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 9,
|
||||
"string": "9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Reallocated_Sector_Ct",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 1,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 51,
|
||||
"string": "PO--CK ",
|
||||
"prefailure": true,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": true
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Seek_Error_Rate",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 1,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 10,
|
||||
"string": "-O-R-- ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": true,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Seek_Time_Performance",
|
||||
"value": 133,
|
||||
"worst": 133,
|
||||
"thresh": 20,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 4,
|
||||
"string": "--S--- ",
|
||||
"prefailure": false,
|
||||
"updated_online": false,
|
||||
"performance": true,
|
||||
"error_rate": false,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 18,
|
||||
"string": "18"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Power_On_Hours",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 18,
|
||||
"string": "-O--C- ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 1730,
|
||||
"string": "1730"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Spin_Retry_Count",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 1,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 18,
|
||||
"string": "-O--C- ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Power_Cycle_Count",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 50,
|
||||
"string": "-O--CK ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": true
|
||||
},
|
||||
"raw": {
|
||||
"value": 9,
|
||||
"string": "9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "Unknown_Attribute",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 25,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 35,
|
||||
"string": "PO---K ",
|
||||
"prefailure": true,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": false,
|
||||
"auto_keep": true
|
||||
},
|
||||
"raw": {
|
||||
"value": 100,
|
||||
"string": "100"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 192,
|
||||
"name": "Power-Off_Retract_Count",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 50,
|
||||
"string": "-O--CK ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": true
|
||||
},
|
||||
"raw": {
|
||||
"value": 329,
|
||||
"string": "329"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 193,
|
||||
"name": "Load_Cycle_Count",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 18,
|
||||
"string": "-O--C- ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 329,
|
||||
"string": "329"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 194,
|
||||
"name": "Temperature_Celsius",
|
||||
"value": 51,
|
||||
"worst": 51,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 2,
|
||||
"string": "-O---- ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 163210330144,
|
||||
"string": "32 (Min/Max 24/38)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 196,
|
||||
"name": "Reallocated_Event_Count",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 50,
|
||||
"string": "-O--CK ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": true,
|
||||
"auto_keep": true
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 197,
|
||||
"name": "Current_Pending_Sector",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 34,
|
||||
"string": "-O---K ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": false,
|
||||
"event_count": false,
|
||||
"auto_keep": true
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 198,
|
||||
"name": "Offline_Uncorrectable",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 8,
|
||||
"string": "---R-- ",
|
||||
"prefailure": false,
|
||||
"updated_online": false,
|
||||
"performance": false,
|
||||
"error_rate": true,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 199,
|
||||
"name": "UDMA_CRC_Error_Count",
|
||||
"value": 100,
|
||||
"worst": 100,
|
||||
"thresh": 0,
|
||||
"when_failed": "",
|
||||
"flags": {
|
||||
"value": 10,
|
||||
"string": "-O-R-- ",
|
||||
"prefailure": false,
|
||||
"updated_online": true,
|
||||
"performance": false,
|
||||
"error_rate": true,
|
||||
"event_count": false,
|
||||
"auto_keep": false
|
||||
},
|
||||
"raw": {
|
||||
"value": 0,
|
||||
"string": "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"power_on_time": {
|
||||
"hours": 1730
|
||||
},
|
||||
"power_cycle_count": 9,
|
||||
"temperature": {
|
||||
"current": 32
|
||||
},
|
||||
"ata_smart_error_log": {
|
||||
"summary": {
|
||||
"revision": 1,
|
||||
"count": 0
|
||||
}
|
||||
},
|
||||
"ata_smart_self_test_log": {
|
||||
"standard": {
|
||||
"revision": 1,
|
||||
"table": [
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1708
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1684
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1661
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1636
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 2,
|
||||
"string": "Extended offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1624
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1541
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1517
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1493
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1469
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1445
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 2,
|
||||
"string": "Extended offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1439
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1373
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1349
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1325
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1301
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1277
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1253
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 2,
|
||||
"string": "Extended offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1252
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1205
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1181
|
||||
},
|
||||
{
|
||||
"type": {
|
||||
"value": 1,
|
||||
"string": "Short offline"
|
||||
},
|
||||
"status": {
|
||||
"value": 0,
|
||||
"string": "Completed without error",
|
||||
"passed": true
|
||||
},
|
||||
"lifetime_hours": 1157
|
||||
}
|
||||
],
|
||||
"count": 21,
|
||||
"error_count_total": 0,
|
||||
"error_count_outdated": 0
|
||||
}
|
||||
},
|
||||
"ata_smart_selective_self_test_log": {
|
||||
"revision": 1,
|
||||
"table": [
|
||||
{
|
||||
"lba_min": 0,
|
||||
"lba_max": 0,
|
||||
"status": {
|
||||
"value": 241,
|
||||
"string": "Not_testing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"lba_min": 0,
|
||||
"lba_max": 0,
|
||||
"status": {
|
||||
"value": 241,
|
||||
"string": "Not_testing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"lba_min": 0,
|
||||
"lba_max": 0,
|
||||
"status": {
|
||||
"value": 241,
|
||||
"string": "Not_testing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"lba_min": 0,
|
||||
"lba_max": 0,
|
||||
"status": {
|
||||
"value": 241,
|
||||
"string": "Not_testing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"lba_min": 0,
|
||||
"lba_max": 0,
|
||||
"status": {
|
||||
"value": 241,
|
||||
"string": "Not_testing"
|
||||
}
|
||||
}
|
||||
],
|
||||
"flags": {
|
||||
"value": 0,
|
||||
"remainder_scan_enabled": false
|
||||
},
|
||||
"power_up_scan_resume_minutes": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package version
|
||||
|
||||
// VERSION is the app-global version string, which will be replaced with a
|
||||
// new value during packaging
|
||||
const VERSION = "1.0.0"
|
||||
@@ -0,0 +1,8 @@
|
||||
package web
|
||||
|
||||
// the following cronjobs need to be defined here:
|
||||
// - get storage information for all approved disks
|
||||
// - run short test against approved disks
|
||||
// - run long test against approved disks
|
||||
// - get S.M.A.R.T. metrics from approved disks
|
||||
// - clean up / resolution for time-series data in sqlite database.
|
||||
@@ -0,0 +1,77 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||
"github.com/jaypipes/ghw"
|
||||
)
|
||||
|
||||
func RetrieveStorageDevices() ([]db.Device, error) {
|
||||
|
||||
block, err := ghw.Block()
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting block storage info: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
approvedDisks := []db.Device{}
|
||||
for _, disk := range block.Disks {
|
||||
//TODO: always allow if in approved list
|
||||
fmt.Printf(" %v\n", disk)
|
||||
|
||||
// ignore optical drives and floppy disks
|
||||
if disk.DriveType == ghw.DRIVE_TYPE_FDD || disk.DriveType == ghw.DRIVE_TYPE_ODD {
|
||||
fmt.Printf(" => Ignore: Optical or floppy disk - (found %s)\n", disk.DriveType.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore removable disks
|
||||
if disk.IsRemovable {
|
||||
fmt.Printf(" => Ignore: Removable disk (%v)\n", disk.IsRemovable)
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore virtual disks & mobile phone storage devices
|
||||
if disk.StorageController == ghw.STORAGE_CONTROLLER_VIRTIO || disk.StorageController == ghw.STORAGE_CONTROLLER_MMC {
|
||||
fmt.Printf(" => Ignore: Virtual/multi-media storage controller - (found %s)\n", disk.StorageController.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// ignore NVMe devices (not currently supported) TBA
|
||||
if disk.StorageController == ghw.STORAGE_CONTROLLER_NVME {
|
||||
fmt.Printf(" => Ignore: NVMe storage controller - (found %s)\n", disk.StorageController.String())
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip unknown storage controllers, not usually S.M.A.R.T compatible.
|
||||
if disk.StorageController == ghw.STORAGE_CONTROLLER_UNKNOWN {
|
||||
fmt.Printf(" => Ignore: Unknown storage controller - (found %s)\n", disk.StorageController.String())
|
||||
continue
|
||||
}
|
||||
|
||||
//TODO: remove if in excluded list
|
||||
|
||||
diskModel := db.Device{
|
||||
WWN: disk.WWN,
|
||||
Manufacturer: disk.Vendor,
|
||||
ModelName: disk.Model,
|
||||
InterfaceType: disk.StorageController.String(),
|
||||
//InterfaceSpeed: string
|
||||
SerialNumber: disk.SerialNumber,
|
||||
Capacity: int64(disk.SizeBytes),
|
||||
//Firmware string
|
||||
//RotationSpeed int
|
||||
|
||||
DeviceName: disk.Name,
|
||||
}
|
||||
if len(diskModel.WWN) == 0 {
|
||||
//(macOS and some other os's) do not provide a WWN, so we're going to fallback to
|
||||
//diskname as identifier if WWN is not present
|
||||
diskModel.WWN = disk.Name
|
||||
}
|
||||
|
||||
approvedDisks = append(approvedDisks, diskModel)
|
||||
}
|
||||
|
||||
return approvedDisks, nil
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/config"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/database"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/metadata"
|
||||
"github.com/analogj/scrutiny/webapp/backend/pkg/models/collector"
|
||||
dbModels "github.com/analogj/scrutiny/webapp/backend/pkg/models/db"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AppEngine struct {
|
||||
Config config.Interface
|
||||
}
|
||||
|
||||
func (ae *AppEngine) Start() error {
|
||||
r := gin.Default()
|
||||
|
||||
r.Use(database.DatabaseHandler(ae.Config.GetString("web.database.location")))
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
api.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
|
||||
//TODO: notifications
|
||||
api.GET("/devices", GetDevicesHandler)
|
||||
api.GET("/summary", GetDevicesSummary)
|
||||
api.POST("/device/:wwn/smart", UploadDeviceSmartData)
|
||||
api.POST("/device/:wwn/selftest", UploadDeviceSelfTestData)
|
||||
|
||||
api.GET("/device/:wwn/details", GetDeviceDetails)
|
||||
}
|
||||
|
||||
//Static request routing
|
||||
r.StaticFS("/web", http.Dir(ae.Config.GetString("web.src.frontend.path")))
|
||||
|
||||
//redirect base url to /web
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/web")
|
||||
})
|
||||
|
||||
//catch-all, serve index page.
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.File(fmt.Sprintf("%s/index.html", ae.Config.GetString("web.src.frontend.path")))
|
||||
})
|
||||
|
||||
return r.Run(fmt.Sprintf("%s:%s", ae.Config.GetString("web.listen.host"), ae.Config.GetString("web.listen.port"))) // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
|
||||
}
|
||||
|
||||
// Get all active disks for processing by collectors
|
||||
func GetDevicesHandler(c *gin.Context) {
|
||||
storageDevices, err := RetrieveStorageDevices()
|
||||
|
||||
db := c.MustGet("DB").(*gorm.DB)
|
||||
for _, dev := range storageDevices {
|
||||
//insert devices into DB if not already there.
|
||||
db.Where(dbModels.Device{WWN: dev.WWN}).FirstOrCreate(&dev)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": storageDevices,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UploadDeviceSmartData(c *gin.Context) {
|
||||
db := c.MustGet("DB").(*gorm.DB)
|
||||
|
||||
var collectorSmartData collector.SmartInfo
|
||||
err := c.BindJSON(&collectorSmartData)
|
||||
if err != nil {
|
||||
//TODO: cannot parse smart data
|
||||
log.Error("Cannot parse SMART data")
|
||||
c.JSON(http.StatusOK, gin.H{"success": false})
|
||||
|
||||
}
|
||||
|
||||
//update the device information if necessary
|
||||
var device dbModels.Device
|
||||
db.Where("wwn = ?", c.Param("wwn")).First(&device)
|
||||
device.UpdateFromCollectorSmartInfo(collectorSmartData)
|
||||
db.Model(&device).Updates(device)
|
||||
|
||||
// insert smart info
|
||||
deviceSmartData := dbModels.Smart{}
|
||||
err = deviceSmartData.FromCollectorSmartInfo(c.Param("wwn"), collectorSmartData)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false})
|
||||
return
|
||||
}
|
||||
db.Create(&deviceSmartData)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
func UploadDeviceSelfTestData(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func GetDeviceDetails(c *gin.Context) {
|
||||
db := c.MustGet("DB").(*gorm.DB)
|
||||
device := dbModels.Device{}
|
||||
|
||||
db.Debug().
|
||||
Preload("SmartResults", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("smarts.created_at DESC").Limit(40)
|
||||
}).
|
||||
Preload("SmartResults.SmartAttributes").
|
||||
Where("wwn = ?", c.Param("wwn")).
|
||||
First(&device)
|
||||
|
||||
device.SquashHistory()
|
||||
device.ApplyMetadataRules()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": device, "lookup": metadata.AtaSmartAttributes})
|
||||
|
||||
}
|
||||
|
||||
func GetDevicesSummary(c *gin.Context) {
|
||||
db := c.MustGet("DB").(*gorm.DB)
|
||||
|
||||
devices := []dbModels.Device{}
|
||||
|
||||
//OLD: cant seem to figure out how to get the latest SmartResults for each Device, so instead
|
||||
// we're going to assume that results were retrieved at the same time, so we'll just get the last x number of results
|
||||
//var devicesCount int
|
||||
//db.Table("devices").Count(&devicesCount)
|
||||
|
||||
//We need the last x (for now all) Smart objects for each Device, so that we can graph Temperature
|
||||
//We also need the last
|
||||
db.Debug().
|
||||
Preload("SmartResults", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("smarts.created_at DESC") //OLD: .Limit(devicesCount)
|
||||
}).
|
||||
//Preload("SmartResults").
|
||||
// Preload("SmartResults.SmartAttributes").
|
||||
Find(&devices)
|
||||
|
||||
//for _, dev := range devices {
|
||||
// log.Printf("===== device: %s\n", dev.WWN)
|
||||
// log.Print(len(dev.SmartResults))
|
||||
//}
|
||||
//a, _ := json.Marshal(devices) //get json byte array
|
||||
//n := len(a) //Find the length of the byte array
|
||||
//s := string(a[:n]) //convert to string
|
||||
//log.Print(s) //write to response
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": devices,
|
||||
})
|
||||
//c.Data(http.StatusOK, "application/json", a)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,48 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
/dist
|
||||
@@ -0,0 +1,70 @@
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ 3rd party credits
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
// Icons
|
||||
Material - https://material.io/tools/icons
|
||||
Dripicons - http://demo.amitjakhu.com/dripicons/
|
||||
Feather - https://feathericons.com/
|
||||
Heroicons - https://github.com/refactoringui/heroicons
|
||||
Iconsmind - https://iconsmind.com/
|
||||
|
||||
// Avatars
|
||||
https://uifaces.co/
|
||||
|
||||
// Mail app
|
||||
Photo by Riccardo Chiarini on Unsplash - https://unsplash.com/photos/2VDa8bnLM8c
|
||||
Photo by Johannes Plenio on Unsplash - https://unsplash.com/photos/RwHv7LgeC7s
|
||||
Photo by Jamie Davies on Unsplash - https://unsplash.com/photos/Hao52Fu9-F8
|
||||
Photo by Christian Joudrey on Unsplash - https://unsplash.com/photos/mWRR1xj95hg
|
||||
|
||||
// Auth pages
|
||||
Photo by Meric Dagli on Unsplash - https://unsplash.com/photos/kZTYGpoeQO0
|
||||
|
||||
// Profile page
|
||||
Photo by Alex Knight on Unsplash - https://unsplash.com/photos/DpPutJwgyW8
|
||||
|
||||
// Cards
|
||||
Photo by Kym Ellis on Unsplash - https://unsplash.com/photos/RPT3AjdXlZc
|
||||
Photo by Patrick Hendry on Unsplash - https://unsplash.com/photos/Qgxk3PQsMiI
|
||||
Photo by Hailey Kean on Unsplash - https://unsplash.com/photos/QxjsOlFNr_4
|
||||
Photo by Nathan Anderson on Unsplash - https://unsplash.com/photos/mG8ShlWrMDI
|
||||
Photo by Adrian Infernus on Unsplash - https://unsplash.com/photos/5apewqWk978
|
||||
Photo by freestocks.org on Unsplash - https://unsplash.com/photos/c73TZ2sIU38
|
||||
Photo by Tim Marshall on Unsplash - https://unsplash.com/photos/PKSCrmZdvwA
|
||||
Photo by Daniel Koponyas on Unsplash - https://unsplash.com/photos/rbiLY6ZwvXQ
|
||||
Photo by John Westrock on Unsplash - https://unsplash.com/photos/LCesauDseu8
|
||||
Photo by Gabriel Sollmann on Unsplash - https://unsplash.com/photos/kFWj9y-tJB4
|
||||
Photo by Kevin Wolf on Unsplash - https://unsplash.com/photos/BJyjgEdNTPs
|
||||
Photo by Luca Bravo on Unsplash - https://unsplash.com/photos/hFzIoD0F_i8
|
||||
Photo by Ian Baldwin on Unsplash - https://unsplash.com/photos/Dlj-SxxTlQ0
|
||||
Photo by Ben Kolde on Unsplash - https://unsplash.com/photos/KRTFIBOfcFw
|
||||
Photo by Chad Peltola on Unsplash - https://unsplash.com/photos/BTvQ2ET_iKc
|
||||
Photo by rocknwool on Unsplash - https://unsplash.com/photos/r56oO1V5oms
|
||||
Photo by Vita Vilcina on Unsplash - https://unsplash.com/photos/KtOid0FLjqU
|
||||
Photo by Jia Ye on Unsplash - https://unsplash.com/photos/y8ZnQqgohLk
|
||||
Photo by Parker Whitson on Unsplash - https://unsplash.com/photos/OlTYIqTjmVM
|
||||
Photo by Dorian Hurst on Unsplash - https://unsplash.com/photos/a9uWPQlIbYc
|
||||
Photo by Everaldo Coelho on Unsplash - https://unsplash.com/photos/KPaSCpklCZw
|
||||
Photo by eberhard grossgasteiger on Unsplash - https://unsplash.com/photos/fh2JefbNlII
|
||||
Photo by Orlova Maria on Unsplash - https://unsplash.com/photos/p8y4dWEMGMU
|
||||
Photo by Jake Blucker on Unsplash - https://unsplash.com/photos/tMzCrBkM99Y
|
||||
Photo by Jerry Zhang on Unsplash - https://unsplash.com/photos/oIBcow6n36s
|
||||
Photo by John Cobb on Unsplash - https://unsplash.com/photos/IE_sifhay7o
|
||||
Photo by Dan Gold on Unsplash - https://unsplash.com/photos/mDlhOIfGxNI
|
||||
Photo by Ana Toma on Unsplash - https://unsplash.com/photos/XsGwe6gYg0c
|
||||
Photo by Andrea on Unsplash - https://unsplash.com/photos/1AWY0N960Sk
|
||||
Photo by Aswin on Unsplash - https://unsplash.com/photos/_roUcFWstas
|
||||
Photo by Justin Kauffman on Unsplash - https://unsplash.com/photos/aWG_dqyhI0A
|
||||
Photo by Barna Bartis on Unsplash - https://unsplash.com/photos/VVoBQqWrvkc
|
||||
Photo by Kyle Hinkson on Unsplash - https://unsplash.com/photos/3439EnvnAGo
|
||||
Photo by Spencer Watson on Unsplash - https://unsplash.com/photos/5TBf16GnHKg
|
||||
Photo by adrian on Unsplash - https://unsplash.com/photos/1wrzvwoK8A4
|
||||
Photo by Christopher Rusev on Unsplash - https://unsplash.com/photos/7gKWgCRixf0
|
||||
Photo by Stephen Leonardi on Unsplash - https://unsplash.com/photos/MDmwQVgDHHM
|
||||
Photo by Dwinanda Nurhanif Mujito on Unsplash - https://unsplash.com/photos/pKT5Mg16w_w
|
||||
Photo by Humphrey Muleba on Unsplash - https://unsplash.com/photos/Zuvf5mxT5fs
|
||||
Photo by adrian on Unsplash - https://unsplash.com/photos/PNRxLFPMyJY
|
||||
Photo by Dahee Son on Unsplash - https://unsplash.com/photos/tV06QVJXVxU
|
||||
Photo by Zachary Kyra-Derksen on Unsplash - https://unsplash.com/photos/vkqS7vLQUtg
|
||||
Photo by Rodrigo Soares on Unsplash - https://unsplash.com/photos/8BFWBUkSqQo
|
||||
@@ -0,0 +1,6 @@
|
||||
Envato Standard License
|
||||
|
||||
Copyright (c) Sercan Yemen <sercanyemen@gmail.com>
|
||||
|
||||
This project is protected by Envato's Standard License. For more information,
|
||||
check the official license page at [https://themeforest.net/licenses/standard](https://themeforest.net/licenses/standard)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Treo - Admin template and Starter project for Angular
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"treo": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/treo",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/favicon-16x16.png",
|
||||
"src/favicon-32x32.png",
|
||||
"src/assets"
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"src/@treo/styles"
|
||||
]
|
||||
},
|
||||
"styles": [
|
||||
"src/styles/vendors.scss",
|
||||
"src/@treo/styles/main.scss",
|
||||
"src/styles/styles.scss",
|
||||
"src/styles/tailwind.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "5mb",
|
||||
"maximumError": "8mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "60kb",
|
||||
"maximumError": "100kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "treo:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "treo:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "treo:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon-16x16.png",
|
||||
"src/favicon-32x32.png",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "treo:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "treo:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "treo"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const {SpecReporter} = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs : [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities : {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect : true,
|
||||
baseUrl : 'http://localhost:4200/',
|
||||
framework : 'jasmine',
|
||||
jasmineNodeOpts : {
|
||||
showColors : true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print : function ()
|
||||
{
|
||||
}
|
||||
},
|
||||
onPrepare()
|
||||
{
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to Treo!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage
|
||||
{
|
||||
navigateTo(): Promise<unknown>
|
||||
{
|
||||
return browser.get(browser.baseUrl) as Promise<unknown>;
|
||||
}
|
||||
|
||||
getTitleText(): Promise<string>
|
||||
{
|
||||
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config)
|
||||
{
|
||||
config.set({
|
||||
basePath : '',
|
||||
frameworks : ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins : [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client : {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir : require('path').join(__dirname, './coverage/treo'),
|
||||
reports : ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
reporters : ['progress', 'kjhtml'],
|
||||
port : 9876,
|
||||
colors : true,
|
||||
logLevel : config.LOG_INFO,
|
||||
autoWatch : true,
|
||||
browsers : ['Chrome'],
|
||||
singleRun : false,
|
||||
restartOnFileChange : true
|
||||
});
|
||||
};
|
||||
Generated
+14972
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "@treo/starter",
|
||||
"version": "1.0.1",
|
||||
"license": "https://themeforest.net/licenses/standard",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --open",
|
||||
"start:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng serve --open",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --prod",
|
||||
"build:prod:mem": "node --max_old_space_size=6144 ./node_modules/@angular/cli/bin/ng build --prod",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"tw": "npm run tw:build && npm run tw:export",
|
||||
"tw:build": "./node_modules/.bin/tailwind build src/tailwind/main.css -c src/tailwind/config.js -o src/styles/tailwind.scss",
|
||||
"tw:export": "npm run tw:export:js && npm run tw:export:scss",
|
||||
"tw:export:js": "node src/@treo/tailwind/export.js -c src/tailwind/config.js -o src/@treo/tailwind/exported/variables.ts",
|
||||
"tw:export:scss": "./node_modules/.bin/tailwind build src/@treo/tailwind/export.css -c src/tailwind/config.js -o src/@treo/tailwind/exported/_variables.scss"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "9.1.4",
|
||||
"@angular/cdk": "9.2.2",
|
||||
"@angular/common": "9.1.4",
|
||||
"@angular/compiler": "9.1.4",
|
||||
"@angular/core": "9.1.4",
|
||||
"@angular/forms": "9.1.4",
|
||||
"@angular/material": "9.2.2",
|
||||
"@angular/material-moment-adapter": "9.2.2",
|
||||
"@angular/platform-browser": "9.1.4",
|
||||
"@angular/platform-browser-dynamic": "9.1.4",
|
||||
"@angular/router": "9.1.4",
|
||||
"@fullcalendar/angular": "4.4.5-beta",
|
||||
"@fullcalendar/core": "4.4.0",
|
||||
"@fullcalendar/daygrid": "4.4.0",
|
||||
"@fullcalendar/interaction": "4.4.0",
|
||||
"@fullcalendar/list": "4.4.0",
|
||||
"@fullcalendar/moment": "4.4.0",
|
||||
"@fullcalendar/rrule": "4.4.0",
|
||||
"@fullcalendar/timegrid": "4.4.0",
|
||||
"apexcharts": "3.19.0",
|
||||
"crypto-js": "3.3.0",
|
||||
"highlight.js": "10.0.1",
|
||||
"lodash": "4.17.15",
|
||||
"moment": "2.24.0",
|
||||
"ng-apexcharts": "1.2.3",
|
||||
"ngx-markdown": "9.0.0",
|
||||
"ngx-quill": "9.1.0",
|
||||
"perfect-scrollbar": "1.5.0",
|
||||
"quill": "1.3.7",
|
||||
"rrule": "2.6.4",
|
||||
"rxjs": "6.5.5",
|
||||
"tslib": "1.11.1",
|
||||
"web-animations-js": "2.3.2",
|
||||
"zone.js": "0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "0.901.4",
|
||||
"@angular/cli": "9.1.4",
|
||||
"@angular/compiler-cli": "9.1.4",
|
||||
"@angular/language-service": "9.1.4",
|
||||
"@types/crypto-js": "3.1.45",
|
||||
"@types/highlight.js": "9.12.3",
|
||||
"@types/jasmine": "3.5.10",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
"@types/lodash": "4.14.150",
|
||||
"@types/node": "12.12.37",
|
||||
"codelyzer": "5.2.2",
|
||||
"jasmine-core": "3.5.0",
|
||||
"jasmine-spec-reporter": "4.2.1",
|
||||
"karma": "5.0.4",
|
||||
"karma-chrome-launcher": "3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "2.1.1",
|
||||
"karma-jasmine": "3.0.3",
|
||||
"karma-jasmine-html-reporter": "1.5.3",
|
||||
"protractor": "5.4.4",
|
||||
"tailwindcss": "1.4.4",
|
||||
"ts-node": "8.3.0",
|
||||
"tslint": "6.1.2",
|
||||
"typescript": "3.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export class TreoAnimationCurves
|
||||
{
|
||||
static STANDARD_CURVE = 'cubic-bezier(0.4, 0.0, 0.2, 1)';
|
||||
static DECELERATION_CURVE = 'cubic-bezier(0.0, 0.0, 0.2, 1)';
|
||||
static ACCELERATION_CURVE = 'cubic-bezier(0.4, 0.0, 1, 1)';
|
||||
static SHARP_CURVE = 'cubic-bezier(0.4, 0.0, 0.6, 1)';
|
||||
}
|
||||
|
||||
export class TreoAnimationDurations
|
||||
{
|
||||
static COMPLEX = '375ms';
|
||||
static ENTERING = '225ms';
|
||||
static EXITING = '195ms';
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Expand / collapse
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const expandCollapse = trigger('expandCollapse',
|
||||
[
|
||||
state('void, collapsed',
|
||||
style({
|
||||
height: '0'
|
||||
})
|
||||
),
|
||||
|
||||
state('*, expanded',
|
||||
style('*')
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void <=> false, collapsed <=> false, expanded <=> false', []),
|
||||
|
||||
// Transition
|
||||
transition('void <=> *, collapsed <=> expanded',
|
||||
animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export { expandCollapse };
|
||||
@@ -0,0 +1,330 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade in
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeIn = trigger('fadeIn',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
opacity: 0
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
opacity: 1
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade in top
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeInTop = trigger('fadeInTop',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(0, -100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade in bottom
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeInBottom = trigger('fadeInBottom',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(0, 100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade in left
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeInLeft = trigger('fadeInLeft',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(-100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade in right
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeInRight = trigger('fadeInRight',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade out
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeOut = trigger('fadeOut',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
opacity: 1
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
opacity: 0
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade out top
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeOutTop = trigger('fadeOutTop',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(0, -100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade out bottom
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeOutBottom = trigger('fadeOutBottom',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(0, 100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade out left
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeOutLeft = trigger('fadeOutLeft',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(-100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Fade out right
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const fadeOutRight = trigger('fadeOutRight',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'translate3d(100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export { fadeIn, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight, fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight };
|
||||
@@ -0,0 +1 @@
|
||||
export * from './public-api';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expandCollapse } from './expand-collapse';
|
||||
import { fadeIn, fadeInBottom, fadeInLeft, fadeInRight, fadeInTop, fadeOut, fadeOutBottom, fadeOutLeft, fadeOutRight, fadeOutTop } from './fade';
|
||||
import { shake } from './shake';
|
||||
import { slideInBottom, slideInLeft, slideInRight, slideInTop, slideOutBottom, slideOutLeft, slideOutRight, slideOutTop } from './slide';
|
||||
import { zoomIn, zoomOut } from './zoom';
|
||||
|
||||
export const TreoAnimations = [
|
||||
expandCollapse,
|
||||
fadeIn, fadeInTop, fadeInBottom, fadeInLeft, fadeInRight,
|
||||
fadeOut, fadeOutTop, fadeOutBottom, fadeOutLeft, fadeOutRight,
|
||||
shake,
|
||||
slideInTop, slideInBottom, slideInLeft, slideInRight,
|
||||
slideOutTop, slideOutBottom, slideOutLeft, slideOutRight,
|
||||
zoomIn, zoomOut
|
||||
];
|
||||
@@ -0,0 +1,73 @@
|
||||
import { animate, keyframes, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Shake
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const shake = trigger('shake',
|
||||
[
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *, * => true',
|
||||
[
|
||||
animate('{{timings}}',
|
||||
keyframes([
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
offset : 0
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset : 0.1
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset : 0.2
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset : 0.3
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset : 0.4
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset : 0.5
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset : 0.6
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset : 0.7
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(10px, 0, 0)',
|
||||
offset : 0.8
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(-10px, 0, 0)',
|
||||
offset : 0.9
|
||||
}),
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
offset : 1
|
||||
})
|
||||
])
|
||||
)
|
||||
],
|
||||
{
|
||||
params: {
|
||||
timings: '0.8s cubic-bezier(0.455, 0.03, 0.515, 0.955)'
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export { shake };
|
||||
@@ -0,0 +1,252 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide in top
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideInTop = trigger('slideInTop',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(0, -100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide in bottom
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideInBottom = trigger('slideInBottom',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(0, 100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide in left
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideInLeft = trigger('slideInLeft',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(-100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide in right
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideInRight = trigger('slideInRight',
|
||||
[
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide out top
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideOutTop = trigger('slideOutTop',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(0, -100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide out bottom
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideOutBottom = trigger('slideOutBottom',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(0, 100%, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide out left
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideOutLeft = trigger('slideOutLeft',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(-100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Slide out right
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const slideOutRight = trigger('slideOutRight',
|
||||
[
|
||||
state('*',
|
||||
style({
|
||||
transform: 'translate3d(0, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
transform: 'translate3d(100%, 0, 0)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export { slideInTop, slideInBottom, slideInLeft, slideInRight, slideOutTop, slideOutBottom, slideOutLeft, slideOutRight };
|
||||
@@ -0,0 +1,73 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { TreoAnimationCurves, TreoAnimationDurations } from '@treo/animations/defaults';
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Zoom in
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const zoomIn = trigger('zoomIn',
|
||||
[
|
||||
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'scale(0.5)'
|
||||
})
|
||||
),
|
||||
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'scale(1)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('void => false', []),
|
||||
|
||||
// Transition
|
||||
transition('void => *', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.ENTERING} ${TreoAnimationCurves.DECELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Zoom out
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
const zoomOut = trigger('zoomOut',
|
||||
[
|
||||
|
||||
state('*',
|
||||
style({
|
||||
opacity : 1,
|
||||
transform: 'scale(1)'
|
||||
})
|
||||
),
|
||||
|
||||
state('void',
|
||||
style({
|
||||
opacity : 0,
|
||||
transform: 'scale(0.5)'
|
||||
})
|
||||
),
|
||||
|
||||
// Prevent the transition if the state is false
|
||||
transition('false => void', []),
|
||||
|
||||
// Transition
|
||||
transition('* => void', animate('{{timings}}'),
|
||||
{
|
||||
params: {
|
||||
timings: `${TreoAnimationDurations.EXITING} ${TreoAnimationCurves.ACCELERATION_CURVE}`
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export { zoomIn, zoomOut };
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- Flippable card -->
|
||||
<ng-container *ngIf="flippable">
|
||||
|
||||
<!-- Front -->
|
||||
<div class="treo-card-front">
|
||||
<ng-content select="[treoCardFront]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Back -->
|
||||
<div class="treo-card-back">
|
||||
<ng-content select="[treoCardBack]"></ng-content>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<!-- Normal card -->
|
||||
<ng-container *ngIf="!flippable">
|
||||
|
||||
<!-- Content -->
|
||||
<ng-content></ng-content>
|
||||
|
||||
<!-- Expansion -->
|
||||
<div class="treo-card-expansion"
|
||||
*ngIf="expanded"
|
||||
[@expandCollapse]>
|
||||
<ng-content select="[treoCardExpansion]"></ng-content>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
@@ -0,0 +1,85 @@
|
||||
@import 'treo';
|
||||
|
||||
treo-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@include treo-elevation('md');
|
||||
|
||||
// Flippable
|
||||
&.treo-card-flippable {
|
||||
border-radius: 0;
|
||||
overflow: visible;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 1s;
|
||||
@include treo-elevation('none');
|
||||
|
||||
&.treo-card-flipped {
|
||||
|
||||
.treo-card-front {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.treo-card-back {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.treo-card-front,
|
||||
.treo-card-back {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
z-index: 10;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.5s ease-out 0s, visibility 0s ease-in 0.2s, opacity 0s ease-in 0.2s;
|
||||
backface-visibility: hidden;
|
||||
@include treo-elevation('md');
|
||||
}
|
||||
|
||||
.treo-card-front {
|
||||
position: relative;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: rotateY(0deg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.treo-card-back {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: rotateY(180deg);
|
||||
overflow: hidden auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
$background: map-get($theme, background);
|
||||
|
||||
treo-card {
|
||||
background: map-get($background, card);
|
||||
|
||||
&.treo-card-flippable {
|
||||
background: transparent;
|
||||
|
||||
.treo-card-front,
|
||||
.treo-card-back {
|
||||
background: map-get($background, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Component, ElementRef, Input, Renderer2, ViewEncapsulation } from '@angular/core';
|
||||
import { TreoAnimations } from '@treo/animations';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-card',
|
||||
templateUrl : './card.component.html',
|
||||
styleUrls : ['./card.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
animations : TreoAnimations,
|
||||
exportAs : 'treoCard'
|
||||
})
|
||||
export class TreoCardComponent
|
||||
{
|
||||
expanded: boolean;
|
||||
flipped: boolean;
|
||||
|
||||
// Private
|
||||
private _flippable: boolean;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {Renderer2} _renderer2
|
||||
* @param {ElementRef} _elementRef
|
||||
*/
|
||||
constructor(
|
||||
private _renderer2: Renderer2,
|
||||
private _elementRef: ElementRef
|
||||
)
|
||||
{
|
||||
// Set the defaults
|
||||
this.expanded = false;
|
||||
this.flippable = false;
|
||||
this.flipped = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter and getter for flippable
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set flippable(value: boolean)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._flippable === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the class name
|
||||
if ( value )
|
||||
{
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-card-flippable');
|
||||
}
|
||||
else
|
||||
{
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-card-flippable');
|
||||
}
|
||||
|
||||
// Store the value
|
||||
this._flippable = value;
|
||||
}
|
||||
|
||||
get flippable(): boolean
|
||||
{
|
||||
return this._flippable;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Expand the details
|
||||
*/
|
||||
expand(): void
|
||||
{
|
||||
this.expanded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse the details
|
||||
*/
|
||||
collapse(): void
|
||||
{
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the expand/collapse status
|
||||
*/
|
||||
toggleExpanded(): void
|
||||
{
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip the card
|
||||
*/
|
||||
flip(): void
|
||||
{
|
||||
// Return if not flippable
|
||||
if ( !this.flippable )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.flipped = !this.flipped;
|
||||
|
||||
// Update the class name
|
||||
if ( this.flipped )
|
||||
{
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-card-flipped');
|
||||
}
|
||||
else
|
||||
{
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-card-flipped');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TreoCardComponent } from '@treo/components/card/card.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TreoCardComponent
|
||||
],
|
||||
imports : [
|
||||
CommonModule
|
||||
],
|
||||
exports : [
|
||||
TreoCardComponent
|
||||
]
|
||||
})
|
||||
export class TreoCardModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@treo/components/card/public-api';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '@treo/components/card/card.component';
|
||||
export * from '@treo/components/card/card.module';
|
||||
@@ -0,0 +1,90 @@
|
||||
<div class="range"
|
||||
(click)="openPickerPanel()"
|
||||
#pickerPanelOrigin>
|
||||
|
||||
<div class="start">
|
||||
<div class="date">{{range.startDate}}</div>
|
||||
<div class="time"
|
||||
*ngIf="range.startTime">{{range.startTime}}</div>
|
||||
</div>
|
||||
|
||||
<div class="separator">-</div>
|
||||
|
||||
<div class="end">
|
||||
<div class="date">{{range.endDate}}</div>
|
||||
<div class="time"
|
||||
*ngIf="range.endTime">{{range.endTime}}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #pickerPanel>
|
||||
|
||||
<!-- Start -->
|
||||
<div class="start">
|
||||
|
||||
<div class="month">
|
||||
<div class="month-header">
|
||||
<button class="previous-button"
|
||||
mat-icon-button
|
||||
(click)="prev()"
|
||||
tabindex="1">
|
||||
<mat-icon [svgIcon]="'chevron_left'"></mat-icon>
|
||||
</button>
|
||||
<div class="month-label">{{getMonthLabel(1)}}</div>
|
||||
</div>
|
||||
<mat-month-view [(activeDate)]="activeDates.month1"
|
||||
[dateFilter]="dateFilter()"
|
||||
[dateClass]="dateClass()"
|
||||
(click)="$event.stopImmediatePropagation()"
|
||||
(selectedChange)="onSelectedDateChange($event)"
|
||||
#matMonthView1>
|
||||
</mat-month-view>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="treo-mat-no-subscript time start-time"
|
||||
*ngIf="timeRange">
|
||||
<input matInput
|
||||
[autocomplete]="'off'"
|
||||
[formControl]="startTimeFormControl"
|
||||
(blur)="updateStartTime($event)"
|
||||
tabindex="3">
|
||||
<mat-label>Start time</mat-label>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- End -->
|
||||
<div class="end">
|
||||
|
||||
<div class="month">
|
||||
<div class="month-header">
|
||||
<div class="month-label">{{getMonthLabel(2)}}</div>
|
||||
<button class="next-button"
|
||||
mat-icon-button
|
||||
(click)="next()"
|
||||
tabindex="2">
|
||||
<mat-icon [svgIcon]="'chevron_right'"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-month-view [(activeDate)]="activeDates.month2"
|
||||
[dateFilter]="dateFilter()"
|
||||
[dateClass]="dateClass()"
|
||||
(click)="$event.stopImmediatePropagation()"
|
||||
(selectedChange)="onSelectedDateChange($event)"
|
||||
#matMonthView2>
|
||||
</mat-month-view>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="treo-mat-no-subscript time end-time"
|
||||
*ngIf="timeRange">
|
||||
<input matInput
|
||||
[formControl]="endTimeFormControl"
|
||||
(blur)="updateEndTime($event)"
|
||||
tabindex="4">
|
||||
<mat-label>End time</mat-label>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
@@ -0,0 +1,351 @@
|
||||
@import 'treo';
|
||||
|
||||
// Variables
|
||||
$body-cell-padding: 2px;
|
||||
|
||||
treo-date-range {
|
||||
display: flex;
|
||||
|
||||
.range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
max-height: 48px;
|
||||
cursor: pointer;
|
||||
|
||||
.start,
|
||||
.end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
border-radius: 5px;
|
||||
border-width: 1px;
|
||||
line-height: 1;
|
||||
|
||||
.date {
|
||||
white-space: nowrap;
|
||||
|
||||
+ .time {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 12px;
|
||||
|
||||
@include treo-breakpoint('xs') {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.treo-date-range-panel {
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
|
||||
.start,
|
||||
.end {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.month {
|
||||
max-width: 196px;
|
||||
min-width: 196px;
|
||||
width: 196px;
|
||||
|
||||
.month-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.previous-button,
|
||||
.next-button {
|
||||
position: absolute;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
min-height: 24px !important;
|
||||
max-height: 24px !important;
|
||||
line-height: 24px !important;
|
||||
|
||||
.mat-icon {
|
||||
@include treo-icon-size(20);
|
||||
}
|
||||
}
|
||||
|
||||
.previous-button {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.month-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
mat-month-view {
|
||||
display: flex;
|
||||
min-height: 188px;
|
||||
|
||||
.mat-calendar-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
tbody {
|
||||
|
||||
tr {
|
||||
|
||||
&[aria-hidden=true] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
|
||||
td:first-child {
|
||||
|
||||
&[aria-hidden=true] {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td.mat-calendar-body-cell {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
padding: $body-cell-padding !important;
|
||||
|
||||
&.treo-date-range {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: $body-cell-padding;
|
||||
right: 0;
|
||||
bottom: $body-cell-padding;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.treo-date-range-start {
|
||||
|
||||
&:before {
|
||||
left: $body-cell-padding;
|
||||
border-radius: 999px 0 0 999px;
|
||||
}
|
||||
|
||||
&.treo-date-range-end,
|
||||
&:last-child {
|
||||
|
||||
&:before {
|
||||
right: $body-cell-padding;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.treo-date-range-end {
|
||||
|
||||
&:before {
|
||||
right: $body-cell-padding;
|
||||
border-radius: 0 999px 999px 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
|
||||
&:before {
|
||||
left: $body-cell-padding;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
|
||||
&:before {
|
||||
border-radius: 999px 0 0 999px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
||||
&:before {
|
||||
border-radius: 0 999px 999px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-calendar-body-cell-content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
td.mat-calendar-body-label {
|
||||
|
||||
+ td.mat-calendar-body-cell {
|
||||
|
||||
&.treo-date-range {
|
||||
|
||||
&:before {
|
||||
border-radius: 999px 0 0 999px;
|
||||
}
|
||||
|
||||
&.treo-date-range-start {
|
||||
|
||||
&.treo-date-range-end {
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
&.treo-date-range-end {
|
||||
|
||||
&:before {
|
||||
left: $body-cell-padding;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 100%;
|
||||
max-width: 196px;
|
||||
}
|
||||
}
|
||||
|
||||
.start {
|
||||
align-items: flex-start;
|
||||
margin-right: 20px;
|
||||
|
||||
.month {
|
||||
|
||||
.month-label {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.end {
|
||||
align-items: flex-end;
|
||||
margin-left: 20px;
|
||||
|
||||
.month {
|
||||
|
||||
.month-label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
$is-dark: map-get($theme, is-dark);
|
||||
|
||||
treo-date-range {
|
||||
|
||||
.range {
|
||||
|
||||
.start,
|
||||
.end {
|
||||
@if ($is-dark) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-color: treo-color('cool-gray', 500);
|
||||
} @else {
|
||||
background-color: treo-color('cool-gray', 50);
|
||||
border-color: treo-color('cool-gray', 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.treo-date-range-panel {
|
||||
background: map-get($background, card);
|
||||
@include treo-elevation('2xl');
|
||||
|
||||
.start,
|
||||
.end {
|
||||
|
||||
.month {
|
||||
|
||||
.month-header {
|
||||
|
||||
.month-label {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
|
||||
mat-month-view {
|
||||
|
||||
.mat-calendar-table {
|
||||
|
||||
tbody {
|
||||
|
||||
tr {
|
||||
|
||||
td,
|
||||
td:hover {
|
||||
|
||||
&.treo-date-range {
|
||||
|
||||
&:before {
|
||||
background-color: map-get($primary, 200);
|
||||
}
|
||||
|
||||
.mat-calendar-body-cell-content {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.treo-date-range-start,
|
||||
&.treo-date-range-end {
|
||||
|
||||
.mat-calendar-body-cell-content {
|
||||
background-color: map-get($primary, default);
|
||||
color: map-get($primary, default-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-calendar-body-today {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
import { ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostBinding, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
|
||||
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { TemplatePortal } from '@angular/cdk/portal';
|
||||
import { MatCalendarCellCssClasses, MatMonthView } from '@angular/material/datepicker';
|
||||
import { Subject } from 'rxjs';
|
||||
import * as moment from 'moment';
|
||||
import { Moment } from 'moment';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-date-range',
|
||||
templateUrl : './date-range.component.html',
|
||||
styleUrls : ['./date-range.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs : 'treoDateRange',
|
||||
providers : [
|
||||
{
|
||||
provide : NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => TreoDateRangeComponent),
|
||||
multi : true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class TreoDateRangeComponent implements ControlValueAccessor, OnInit, OnDestroy
|
||||
{
|
||||
// Range changed
|
||||
@Output()
|
||||
readonly rangeChanged: EventEmitter<{ start: string, end: string }>;
|
||||
|
||||
activeDates: { month1: Moment, month2: Moment };
|
||||
setWhichDate: 'start' | 'end';
|
||||
startTimeFormControl: FormControl;
|
||||
endTimeFormControl: FormControl;
|
||||
|
||||
// Private
|
||||
@HostBinding('class.treo-date-range')
|
||||
private _defaultClassNames;
|
||||
|
||||
@ViewChild('matMonthView1')
|
||||
private _matMonthView1: MatMonthView<any>;
|
||||
|
||||
@ViewChild('matMonthView2')
|
||||
private _matMonthView2: MatMonthView<any>;
|
||||
|
||||
@ViewChild('pickerPanelOrigin', {read: ElementRef})
|
||||
private _pickerPanelOrigin: ElementRef;
|
||||
|
||||
@ViewChild('pickerPanel')
|
||||
private _pickerPanel: TemplateRef<any>;
|
||||
|
||||
private _dateFormat: string;
|
||||
private _onChange: (value: any) => void;
|
||||
private _onTouched: (value: any) => void;
|
||||
private _programmaticChange: boolean;
|
||||
private _range: { start: Moment, end: Moment };
|
||||
private _timeFormat: string;
|
||||
private _timeRange: boolean;
|
||||
private readonly _timeRegExp: RegExp;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
* @param {ElementRef} _elementRef
|
||||
* @param {Overlay} _overlay
|
||||
* @param {Renderer2} _renderer2
|
||||
* @param {ViewContainerRef} _viewContainerRef
|
||||
*/
|
||||
constructor(
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _elementRef: ElementRef,
|
||||
private _overlay: Overlay,
|
||||
private _renderer2: Renderer2,
|
||||
private _viewContainerRef: ViewContainerRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._defaultClassNames = true;
|
||||
this._onChange = () => {
|
||||
};
|
||||
this._onTouched = () => {
|
||||
};
|
||||
this._range = {
|
||||
start: null,
|
||||
end : null
|
||||
};
|
||||
this._timeRegExp = new RegExp('^(0[0-9]|1[0-9]|2[0-4]|[0-9]):([0-5][0-9])(A|(?:AM)|P|(?:PM))?$', 'i');
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.activeDates = {
|
||||
month1: null,
|
||||
month2: null
|
||||
};
|
||||
this.dateFormat = 'DD/MM/YYYY';
|
||||
this.rangeChanged = new EventEmitter();
|
||||
this.setWhichDate = 'start';
|
||||
this.timeFormat = '12';
|
||||
|
||||
// Initialize the component
|
||||
this._init();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter and getter for dateFormat input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set dateFormat(value: string)
|
||||
{
|
||||
// Return, if the values are the same
|
||||
if ( this._dateFormat === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the value
|
||||
this._dateFormat = value;
|
||||
}
|
||||
|
||||
get dateFormat(): string
|
||||
{
|
||||
return this._dateFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for timeFormat input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set timeFormat(value: string)
|
||||
{
|
||||
// Return, if the values are the same
|
||||
if ( this._timeFormat === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set format based on the time format input
|
||||
this._timeFormat = value === '12' ? 'hh:mmA' : 'HH:mm';
|
||||
}
|
||||
|
||||
get timeFormat(): string
|
||||
{
|
||||
return this._timeFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for timeRange input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set timeRange(value: boolean)
|
||||
{
|
||||
// Return, if the values are the same
|
||||
if ( this._timeRange === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the value
|
||||
this._timeRange = value;
|
||||
|
||||
// If the time range turned off...
|
||||
if ( !value )
|
||||
{
|
||||
this.range = {
|
||||
start: this._range.start.clone().startOf('day'),
|
||||
end : this._range.end.clone().endOf('day')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get timeRange(): boolean
|
||||
{
|
||||
return this._timeRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for range input
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set range(value)
|
||||
{
|
||||
if ( !value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the value is an object and has 'start' and 'end' values
|
||||
if ( !value.start || !value.end )
|
||||
{
|
||||
console.error('Range input must have "start" and "end" properties!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we are setting an individual date or both of them
|
||||
const whichDate = value.whichDate || null;
|
||||
|
||||
// Get the start and end dates as moment
|
||||
const start = moment(value.start);
|
||||
const end = moment(value.end);
|
||||
|
||||
// If we are only setting the start date...
|
||||
if ( whichDate === 'start' )
|
||||
{
|
||||
// Set the start date
|
||||
this._range.start = start.clone();
|
||||
|
||||
// If the selected start date is after the end date...
|
||||
if ( this._range.start.isAfter(this._range.end) )
|
||||
{
|
||||
// Set the end date to the start date but keep the end date's time
|
||||
const endDate = start.clone().hours(this._range.end.hours()).minutes(this._range.end.minutes()).seconds(this._range.end.seconds());
|
||||
|
||||
// Test this new end date to see if it's ahead of the start date
|
||||
if ( this._range.start.isBefore(endDate) )
|
||||
{
|
||||
// If it's, set the new end date
|
||||
this._range.end = endDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, set the end date same as the start date
|
||||
this._range.end = start.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are only setting the end date...
|
||||
if ( whichDate === 'end' )
|
||||
{
|
||||
// Set the end date
|
||||
this._range.end = end.clone();
|
||||
|
||||
// If the selected end date is before the start date...
|
||||
if ( this._range.start.isAfter(this._range.end) )
|
||||
{
|
||||
// Set the start date to the end date but keep the start date's time
|
||||
const startDate = end.clone().hours(this._range.start.hours()).minutes(this._range.start.minutes()).seconds(this._range.start.seconds());
|
||||
|
||||
// Test this new end date to see if it's ahead of the start date
|
||||
if ( this._range.end.isAfter(startDate) )
|
||||
{
|
||||
// If it's, set the new start date
|
||||
this._range.start = startDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, set the start date same as the end date
|
||||
this._range.start = end.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we are setting both dates...
|
||||
if ( !whichDate )
|
||||
{
|
||||
// Set the start date
|
||||
this._range.start = start.clone();
|
||||
|
||||
// If the start date is before the end date, set the end date as normal.
|
||||
// If the start date is after the end date, set the end date same as the start date.
|
||||
this._range.end = start.isBefore(end) ? end.clone() : start.clone();
|
||||
}
|
||||
|
||||
// Prepare another range object that holds the ISO formatted range dates
|
||||
const range = {
|
||||
start: this._range.start.clone().toISOString(),
|
||||
end : this._range.end.clone().toISOString()
|
||||
};
|
||||
|
||||
// Emit the range changed event with the range
|
||||
this.rangeChanged.emit(range);
|
||||
|
||||
// Update the model with the range if the change was not a programmatic change
|
||||
// Because programmatic changes trigger writeValue which triggers onChange and onTouched
|
||||
// internally causing them to trigger twice which breaks the form's pristine and touched
|
||||
// statuses.
|
||||
if ( !this._programmaticChange )
|
||||
{
|
||||
this._onTouched(range);
|
||||
this._onChange(range);
|
||||
}
|
||||
|
||||
// Set the active dates
|
||||
this.activeDates = {
|
||||
month1: this._range.start.clone(),
|
||||
month2: this._range.start.clone().add(1, 'month')
|
||||
};
|
||||
|
||||
// Set the time form controls
|
||||
this.startTimeFormControl.setValue(this._range.start.clone().format(this._timeFormat).toString());
|
||||
this.endTimeFormControl.setValue(this._range.end.clone().format(this._timeFormat).toString());
|
||||
|
||||
// Run ngAfterContentInit on month views to trigger
|
||||
// re-render on month views if they are available
|
||||
if ( this._matMonthView1 && this._matMonthView2 )
|
||||
{
|
||||
this._matMonthView1.ngAfterContentInit();
|
||||
this._matMonthView2.ngAfterContentInit();
|
||||
}
|
||||
|
||||
// Reset the programmatic change status
|
||||
this._programmaticChange = false;
|
||||
}
|
||||
|
||||
get range(): any
|
||||
{
|
||||
// Clone the range start and end
|
||||
const start = this._range.start.clone();
|
||||
const end = this._range.end.clone();
|
||||
|
||||
// Build and return the range object
|
||||
return {
|
||||
startDate: start.clone().format(this.dateFormat),
|
||||
startTime: this.timeRange ? start.clone().format(this.timeFormat) : null,
|
||||
endDate : end.clone().format(this.dateFormat),
|
||||
endTime : this.timeRange ? end.clone().format(this.timeFormat) : null
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Control Value Accessor
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update the form model on change
|
||||
*
|
||||
* @param fn
|
||||
*/
|
||||
registerOnChange(fn: any): void
|
||||
{
|
||||
this._onChange = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the form model on blur
|
||||
*
|
||||
* @param fn
|
||||
*/
|
||||
registerOnTouched(fn: any): void
|
||||
{
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to view from model when the form model changes programmatically
|
||||
*
|
||||
* @param range
|
||||
*/
|
||||
writeValue(range: { start: string, end: string }): void
|
||||
{
|
||||
// Set this change as a programmatic one
|
||||
this._programmaticChange = true;
|
||||
|
||||
// Set the range
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
|
||||
// @ TODO: Workaround until "angular/issues/20007" resolved
|
||||
this.writeValue = () => {
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _init(): void
|
||||
{
|
||||
// Start and end time form controls
|
||||
this.startTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
|
||||
this.endTimeFormControl = new FormControl('', [Validators.pattern(this._timeRegExp)]);
|
||||
|
||||
// Set the default range
|
||||
this._programmaticChange = true;
|
||||
this.range = {
|
||||
start: moment().startOf('day').toISOString(),
|
||||
end : moment().add(1, 'day').endOf('day').toISOString()
|
||||
};
|
||||
|
||||
// Set the default time range
|
||||
this._programmaticChange = true;
|
||||
this.timeRange = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the time from the inputs
|
||||
*
|
||||
* @param value
|
||||
* @private
|
||||
*/
|
||||
private _parseTime(value: string): Moment
|
||||
{
|
||||
// Parse the time using the time regexp
|
||||
const timeArr = value.split(this._timeRegExp).filter((part) => part !== '');
|
||||
|
||||
// Get the meridiem
|
||||
const meridiem = timeArr[2] || null;
|
||||
|
||||
// If meridiem exists...
|
||||
if ( meridiem )
|
||||
{
|
||||
// Create a moment using 12-hours format and return it
|
||||
return moment(value, 'hh:mmA').seconds(0);
|
||||
}
|
||||
|
||||
// If meridiem doesn't exist, create a moment using 24-hours format and return in
|
||||
return moment(value, 'HH:mm').seconds(0);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open the picker panel
|
||||
*/
|
||||
openPickerPanel(): void
|
||||
{
|
||||
// Create the overlay
|
||||
const overlayRef = this._overlay.create({
|
||||
panelClass : 'treo-date-range-panel',
|
||||
backdropClass : '',
|
||||
hasBackdrop : true,
|
||||
scrollStrategy : this._overlay.scrollStrategies.reposition(),
|
||||
positionStrategy: this._overlay.position()
|
||||
.flexibleConnectedTo(this._pickerPanelOrigin)
|
||||
.withPositions([
|
||||
{
|
||||
originX : 'start',
|
||||
originY : 'bottom',
|
||||
overlayX: 'start',
|
||||
overlayY: 'top',
|
||||
offsetY : 8
|
||||
},
|
||||
{
|
||||
originX : 'start',
|
||||
originY : 'top',
|
||||
overlayX: 'start',
|
||||
overlayY: 'bottom',
|
||||
offsetY : -8
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
// Create a portal from the template
|
||||
const templatePortal = new TemplatePortal(this._pickerPanel, this._viewContainerRef);
|
||||
|
||||
// On backdrop click
|
||||
overlayRef.backdropClick().subscribe(() => {
|
||||
|
||||
// If template portal exists and attached...
|
||||
if ( templatePortal && templatePortal.isAttached )
|
||||
{
|
||||
// Detach it
|
||||
templatePortal.detach();
|
||||
}
|
||||
|
||||
// If overlay exists and attached...
|
||||
if ( overlayRef && overlayRef.hasAttached() )
|
||||
{
|
||||
// Detach it
|
||||
overlayRef.detach();
|
||||
overlayRef.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Attach the portal to the overlay
|
||||
overlayRef.attach(templatePortal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get month label
|
||||
*
|
||||
* @param month
|
||||
*/
|
||||
getMonthLabel(month: number): string
|
||||
{
|
||||
if ( month === 1 )
|
||||
{
|
||||
return this.activeDates.month1.clone().format('MMMM Y');
|
||||
}
|
||||
|
||||
return this.activeDates.month2.clone().format('MMMM Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Date class function to add/remove class names to calendar days
|
||||
*/
|
||||
dateClass(): any
|
||||
{
|
||||
return (date: Moment): MatCalendarCellCssClasses => {
|
||||
|
||||
// If the date is both start and end date...
|
||||
if ( date.isSame(this._range.start, 'day') && date.isSame(this._range.end, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-start', 'treo-date-range-end'];
|
||||
}
|
||||
|
||||
// If the date is the start date...
|
||||
if ( date.isSame(this._range.start, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-start'];
|
||||
}
|
||||
|
||||
// If the date is the end date...
|
||||
if ( date.isSame(this._range.end, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-end'];
|
||||
}
|
||||
|
||||
// If the date is in between start and end dates...
|
||||
if ( date.isBetween(this._range.start, this._range.end, 'day') )
|
||||
{
|
||||
return ['treo-date-range', 'treo-date-range-mid'];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Date filter to enable/disable calendar days
|
||||
*/
|
||||
dateFilter(): any
|
||||
{
|
||||
return (date: Moment): boolean => {
|
||||
|
||||
// If we are selecting the end date, disable all the dates that comes before the start date
|
||||
return !(this.setWhichDate === 'end' && date.isBefore(this._range.start, 'day'));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* On selected date change
|
||||
*
|
||||
* @param date
|
||||
*/
|
||||
onSelectedDateChange(date: Moment): void
|
||||
{
|
||||
// Create a new range object
|
||||
const newRange = {
|
||||
start : this._range.start.clone().toISOString(),
|
||||
end : this._range.end.clone().toISOString(),
|
||||
whichDate: null
|
||||
};
|
||||
|
||||
// Replace either the start or the end date with the new one
|
||||
// depending on which date we are setting
|
||||
if ( this.setWhichDate === 'start' )
|
||||
{
|
||||
newRange.start = moment(newRange.start).year(date.year()).month(date.month()).date(date.date()).toISOString();
|
||||
}
|
||||
else
|
||||
{
|
||||
newRange.end = moment(newRange.end).year(date.year()).month(date.month()).date(date.date()).toISOString();
|
||||
}
|
||||
|
||||
// Append the which date to the new range object
|
||||
newRange.whichDate = this.setWhichDate;
|
||||
|
||||
// Switch which date to set on the next run
|
||||
this.setWhichDate = this.setWhichDate === 'start' ? 'end' : 'start';
|
||||
|
||||
// Set the range
|
||||
this.range = newRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to previous month on both views
|
||||
*/
|
||||
prev(): void
|
||||
{
|
||||
this.activeDates.month1 = moment(this.activeDates.month1).subtract(1, 'month');
|
||||
this.activeDates.month2 = moment(this.activeDates.month2).subtract(1, 'month');
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to next month on both views
|
||||
*/
|
||||
next(): void
|
||||
{
|
||||
this.activeDates.month1 = moment(this.activeDates.month1).add(1, 'month');
|
||||
this.activeDates.month2 = moment(this.activeDates.month2).add(1, 'month');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the start time
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
updateStartTime(event): void
|
||||
{
|
||||
// Parse the time
|
||||
const parsedTime = this._parseTime(event.target.value);
|
||||
|
||||
// Go back to the previous value if the form control is not valid
|
||||
if ( this.startTimeFormControl.invalid )
|
||||
{
|
||||
// Override the time
|
||||
const time = this._range.start.clone().format(this._timeFormat);
|
||||
|
||||
// Set the time
|
||||
this.startTimeFormControl.setValue(time);
|
||||
|
||||
// Do not update the range
|
||||
return;
|
||||
}
|
||||
|
||||
// Append the new time to the start date
|
||||
const startDate = this._range.start.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
|
||||
|
||||
// If the new start date is after the current end date,
|
||||
// use the end date's time and set the start date again
|
||||
if ( startDate.isAfter(this._range.end) )
|
||||
{
|
||||
const endDateHours = this._range.end.hours();
|
||||
const endDateMinutes = this._range.end.minutes();
|
||||
|
||||
// Set the start date
|
||||
startDate.hours(endDateHours).minutes(endDateMinutes);
|
||||
}
|
||||
|
||||
// If everything is okay, set the new date
|
||||
this.range = {
|
||||
start : startDate.toISOString(),
|
||||
end : this._range.end.clone().toISOString(),
|
||||
whichDate: 'start'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the end time
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
updateEndTime(event): void
|
||||
{
|
||||
// Parse the time
|
||||
const parsedTime = this._parseTime(event.target.value);
|
||||
|
||||
// Go back to the previous value if the form control is not valid
|
||||
if ( this.endTimeFormControl.invalid )
|
||||
{
|
||||
// Override the time
|
||||
const time = this._range.end.clone().format(this._timeFormat);
|
||||
|
||||
// Set the time
|
||||
this.endTimeFormControl.setValue(time);
|
||||
|
||||
// Do not update the range
|
||||
return;
|
||||
}
|
||||
|
||||
// Append the new time to the end date
|
||||
const endDate = this._range.end.clone().hours(parsedTime.hours()).minutes(parsedTime.minutes());
|
||||
|
||||
// If the new end date is before the current start date,
|
||||
// use the start date's time and set the end date again
|
||||
if ( endDate.isBefore(this._range.start) )
|
||||
{
|
||||
const startDateHours = this._range.start.hours();
|
||||
const startDateMinutes = this._range.start.minutes();
|
||||
|
||||
// Set the end date
|
||||
endDate.hours(startDateHours).minutes(startDateMinutes);
|
||||
}
|
||||
|
||||
// If everything is okay, set the new date
|
||||
this.range = {
|
||||
start : this._range.start.clone().toISOString(),
|
||||
end : endDate.toISOString(),
|
||||
whichDate: 'end'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMomentDateModule } from '@angular/material-moment-adapter';
|
||||
import { TreoDateRangeComponent } from '@treo/components/date-range/date-range.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TreoDateRangeComponent
|
||||
],
|
||||
imports : [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatDatepickerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatMomentDateModule
|
||||
],
|
||||
exports : [
|
||||
TreoDateRangeComponent
|
||||
]
|
||||
})
|
||||
export class TreoDateRangeModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@treo/components/date-range/public-api';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '@treo/components/date-range/date-range.component';
|
||||
export * from '@treo/components/date-range/date-range.module';
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="treo-drawer-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
@import 'treo';
|
||||
|
||||
$treo-drawer-width: 320;
|
||||
|
||||
treo-drawer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
width: #{$treo-drawer-width}px;
|
||||
min-width: #{$treo-drawer-width}px;
|
||||
max-width: #{$treo-drawer-width}px;
|
||||
z-index: 300;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .35);
|
||||
|
||||
// Animations
|
||||
&.treo-drawer-animations-enabled {
|
||||
transition-duration: 400ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
transition-property: visibility, margin-left, margin-right, transform, width, max-width, min-width;
|
||||
|
||||
.treo-drawer-content {
|
||||
transition-duration: 400ms;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
transition-property: width, max-width, min-width;
|
||||
}
|
||||
}
|
||||
|
||||
// Over mode
|
||||
&.treo-drawer-mode-over {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
// Fixed mode
|
||||
&.treo-drawer-fixed {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
// Left position
|
||||
&.treo-drawer-position-left {
|
||||
|
||||
// Side mode
|
||||
&.treo-drawer-mode-side {
|
||||
margin-left: #{$treo-drawer-width}px;
|
||||
|
||||
&.treo-drawer-opened {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Over mode
|
||||
&.treo-drawer-mode-over {
|
||||
left: 0;
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
|
||||
&.treo-drawer-opened {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
.treo-drawer-content {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Right position
|
||||
&.treo-drawer-position-right {
|
||||
|
||||
// Side mode
|
||||
&.treo-drawer-mode-side {
|
||||
margin-right: -#{$treo-drawer-width}px;
|
||||
|
||||
&.treo-drawer-opened {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Over mode
|
||||
&.treo-drawer-mode-over {
|
||||
right: 0;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
|
||||
&.treo-drawer-opened {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
.treo-drawer-content {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
.treo-drawer-content {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay
|
||||
.treo-drawer-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 299;
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
// Fixed mode
|
||||
&.treo-drawer-overlay-fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
// Transparent overlay
|
||||
&.treo-drawer-overlay-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
|
||||
treo-drawer {
|
||||
background: map-get($background, card);
|
||||
|
||||
.treo-drawer-content {
|
||||
background: map-get($background, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
import { Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation } from '@angular/core';
|
||||
import { animate, AnimationBuilder, AnimationPlayer, style } from '@angular/animations';
|
||||
import { TreoDrawerMode, TreoDrawerPosition } from '@treo/components/drawer/drawer.types';
|
||||
import { TreoDrawerService } from '@treo/components/drawer/drawer.service';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-drawer',
|
||||
templateUrl : './drawer.component.html',
|
||||
styleUrls : ['./drawer.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs : 'treoDrawer'
|
||||
})
|
||||
export class TreoDrawerComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _fixed: boolean;
|
||||
private _mode: TreoDrawerMode;
|
||||
private _opened: boolean | '';
|
||||
private _overlay: HTMLElement | null;
|
||||
private _player: AnimationPlayer;
|
||||
private _position: TreoDrawerPosition;
|
||||
private _transparentOverlay: boolean | '';
|
||||
|
||||
// On fixed changed
|
||||
@Output()
|
||||
readonly fixedChanged: EventEmitter<boolean>;
|
||||
|
||||
// On mode changed
|
||||
@Output()
|
||||
readonly modeChanged: EventEmitter<TreoDrawerMode>;
|
||||
|
||||
// On opened changed
|
||||
@Output()
|
||||
readonly openedChanged: EventEmitter<boolean | ''>;
|
||||
|
||||
// On position changed
|
||||
@Output()
|
||||
readonly positionChanged: EventEmitter<TreoDrawerPosition>;
|
||||
|
||||
@HostBinding('class.treo-drawer-animations-enabled')
|
||||
private _animationsEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {AnimationBuilder} _animationBuilder
|
||||
* @param {TreoDrawerService} _treoDrawerService
|
||||
* @param {ElementRef} _elementRef
|
||||
* @param {Renderer2} _renderer2
|
||||
*/
|
||||
constructor(
|
||||
private _animationBuilder: AnimationBuilder,
|
||||
private _treoDrawerService: TreoDrawerService,
|
||||
private _elementRef: ElementRef,
|
||||
private _renderer2: Renderer2
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._animationsEnabled = false;
|
||||
this._overlay = null;
|
||||
|
||||
// Set the defaults
|
||||
this.fixedChanged = new EventEmitter<boolean>();
|
||||
this.modeChanged = new EventEmitter<TreoDrawerMode>();
|
||||
this.openedChanged = new EventEmitter<boolean | ''>();
|
||||
this.positionChanged = new EventEmitter<TreoDrawerPosition>();
|
||||
|
||||
this.fixed = false;
|
||||
this.mode = 'side';
|
||||
this.opened = false;
|
||||
this.position = 'left';
|
||||
this.transparentOverlay = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter & getter for fixed
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set fixed(value: boolean)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._fixed === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the fixed value
|
||||
this._fixed = value;
|
||||
|
||||
// Update the class
|
||||
if ( this.fixed )
|
||||
{
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-fixed');
|
||||
}
|
||||
else
|
||||
{
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-fixed');
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this.fixedChanged.next(this.fixed);
|
||||
}
|
||||
|
||||
get fixed(): boolean
|
||||
{
|
||||
return this._fixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter & getter for mode
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set mode(value: TreoDrawerMode)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._mode === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the animations
|
||||
this._disableAnimations();
|
||||
|
||||
// If the mode changes: 'over -> side'
|
||||
if ( this.mode === 'over' && value === 'side' )
|
||||
{
|
||||
// Hide the overlay
|
||||
this._hideOverlay();
|
||||
}
|
||||
|
||||
// If the mode changes: 'side -> over'
|
||||
if ( this.mode === 'side' && value === 'over' )
|
||||
{
|
||||
// If the drawer is opened
|
||||
if ( this.opened )
|
||||
{
|
||||
// Show the overlay
|
||||
this._showOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
let modeClassName;
|
||||
|
||||
// Remove the previous mode class
|
||||
modeClassName = 'treo-drawer-mode-' + this.mode;
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, modeClassName);
|
||||
|
||||
// Store the mode
|
||||
this._mode = value;
|
||||
|
||||
// Add the new mode class
|
||||
modeClassName = 'treo-drawer-mode-' + this.mode;
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, modeClassName);
|
||||
|
||||
// Execute the observable
|
||||
this.modeChanged.next(this.mode);
|
||||
|
||||
// Enable the animations after a delay
|
||||
// The delay must be bigger than the current transition-duration
|
||||
// to make sure nothing will be animated while the mode changing
|
||||
setTimeout(() => {
|
||||
this._enableAnimations();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
get mode(): TreoDrawerMode
|
||||
{
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter & getter for opened
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set opened(value: boolean | '')
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._opened === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the provided value is an empty string,
|
||||
// take that as a 'true'
|
||||
if ( value === '' )
|
||||
{
|
||||
value = true;
|
||||
}
|
||||
|
||||
// Set the opened value
|
||||
this._opened = value;
|
||||
|
||||
// If the drawer opened, and the mode
|
||||
// is 'over', show the overlay
|
||||
if ( this.mode === 'over' )
|
||||
{
|
||||
if ( this._opened )
|
||||
{
|
||||
this._showOverlay();
|
||||
}
|
||||
else
|
||||
{
|
||||
this._hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Update opened classes
|
||||
if ( this.opened )
|
||||
{
|
||||
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'visible');
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-opened');
|
||||
}
|
||||
else
|
||||
{
|
||||
this._renderer2.setStyle(this._elementRef.nativeElement, 'visibility', 'hidden');
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-opened');
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this.openedChanged.next(this.opened);
|
||||
}
|
||||
|
||||
get opened(): boolean | ''
|
||||
{
|
||||
return this._opened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter & getter for position
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set position(value: TreoDrawerPosition)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._position === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let positionClassName;
|
||||
|
||||
// Remove the previous position class
|
||||
positionClassName = 'treo-drawer-position-' + this.position;
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, positionClassName);
|
||||
|
||||
// Store the position
|
||||
this._position = value;
|
||||
|
||||
// Add the new position class
|
||||
positionClassName = 'treo-drawer-position-' + this.position;
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, positionClassName);
|
||||
|
||||
// Execute the observable
|
||||
this.positionChanged.next(this.position);
|
||||
}
|
||||
|
||||
get position(): TreoDrawerPosition
|
||||
{
|
||||
return this._position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter & getter for transparent overlay
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set transparentOverlay(value: boolean | '')
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._opened === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the provided value is an empty string,
|
||||
// take that as a 'true' and set the opened value
|
||||
if ( value === '' )
|
||||
{
|
||||
// Set the opened value
|
||||
this._transparentOverlay = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the transparent overlay value
|
||||
this._transparentOverlay = value;
|
||||
}
|
||||
}
|
||||
|
||||
get transparentOverlay(): boolean | ''
|
||||
{
|
||||
return this._transparentOverlay;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Register the drawer
|
||||
this._treoDrawerService.registerComponent(this.name, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Deregister the drawer from the registry
|
||||
this._treoDrawerService.deregisterComponent(this.name);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Enable the animations
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _enableAnimations(): void
|
||||
{
|
||||
// If the animations are already enabled, return...
|
||||
if ( this._animationsEnabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable the animations
|
||||
this._animationsEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the animations
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _disableAnimations(): void
|
||||
{
|
||||
// If the animations are already disabled, return...
|
||||
if ( !this._animationsEnabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the animations
|
||||
this._animationsEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the backdrop
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _showOverlay(): void
|
||||
{
|
||||
// Create the backdrop element
|
||||
this._overlay = this._renderer2.createElement('div');
|
||||
|
||||
// Add a class to the backdrop element
|
||||
this._overlay.classList.add('treo-drawer-overlay');
|
||||
|
||||
// Add a class depending on the fixed option
|
||||
if ( this.fixed )
|
||||
{
|
||||
this._overlay.classList.add('treo-drawer-overlay-fixed');
|
||||
}
|
||||
|
||||
// Add a class depending on the transparentOverlay option
|
||||
if ( this.transparentOverlay )
|
||||
{
|
||||
this._overlay.classList.add('treo-drawer-overlay-transparent');
|
||||
}
|
||||
|
||||
// Append the backdrop to the parent of the drawer
|
||||
this._renderer2.appendChild(this._elementRef.nativeElement.parentElement, this._overlay);
|
||||
|
||||
// Create the enter animation and attach it to the player
|
||||
this._player =
|
||||
this._animationBuilder
|
||||
.build([
|
||||
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 1}))
|
||||
]).create(this._overlay);
|
||||
|
||||
// Play the animation
|
||||
this._player.play();
|
||||
|
||||
// Add an event listener to the overlay
|
||||
this._overlay.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the backdrop
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _hideOverlay(): void
|
||||
{
|
||||
if ( !this._overlay )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the leave animation and attach it to the player
|
||||
this._player =
|
||||
this._animationBuilder
|
||||
.build([
|
||||
animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)', style({opacity: 0}))
|
||||
]).create(this._overlay);
|
||||
|
||||
// Play the animation
|
||||
this._player.play();
|
||||
|
||||
// Once the animation is done...
|
||||
this._player.onDone(() => {
|
||||
|
||||
// If the backdrop still exists...
|
||||
if ( this._overlay )
|
||||
{
|
||||
// Remove the backdrop
|
||||
this._overlay.parentNode.removeChild(this._overlay);
|
||||
this._overlay = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On mouseenter
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
@HostListener('mouseenter')
|
||||
private _onMouseenter(): void
|
||||
{
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Add a class
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-drawer-hover');
|
||||
}
|
||||
|
||||
/**
|
||||
* On mouseleave
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
@HostListener('mouseleave')
|
||||
private _onMouseleave(): void
|
||||
{
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Remove the class
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-drawer-hover');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open the drawer
|
||||
*/
|
||||
open(): void
|
||||
{
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Open
|
||||
this.opened = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the drawer
|
||||
*/
|
||||
close(): void
|
||||
{
|
||||
// Enable the animations
|
||||
this._enableAnimations();
|
||||
|
||||
// Close
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the opened status
|
||||
*/
|
||||
toggle(): void
|
||||
{
|
||||
// Toggle
|
||||
if ( this.opened )
|
||||
{
|
||||
this.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TreoDrawerComponent } from '@treo/components/drawer/drawer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TreoDrawerComponent
|
||||
],
|
||||
imports : [
|
||||
CommonModule
|
||||
],
|
||||
exports : [
|
||||
TreoDrawerComponent
|
||||
]
|
||||
})
|
||||
export class TreoDrawerModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TreoDrawerComponent } from '@treo/components/drawer/drawer.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TreoDrawerService
|
||||
{
|
||||
// Private
|
||||
private _componentRegistry: Map<string, TreoDrawerComponent>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
// Set the defaults
|
||||
this._componentRegistry = new Map<string, TreoDrawerComponent>();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register drawer component
|
||||
*
|
||||
* @param name
|
||||
* @param component
|
||||
*/
|
||||
registerComponent(name: string, component: TreoDrawerComponent): void
|
||||
{
|
||||
this._componentRegistry.set(name, component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister drawer component
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
deregisterComponent(name: string): void
|
||||
{
|
||||
this._componentRegistry.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get drawer component from the registry
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
getComponent(name: string): TreoDrawerComponent
|
||||
{
|
||||
return this._componentRegistry.get(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type TreoDrawerMode = 'over' | 'side';
|
||||
export type TreoDrawerPosition = 'left' | 'right';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@treo/components/drawer/public-api';
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from '@treo/components/drawer/drawer.component';
|
||||
export * from '@treo/components/drawer/drawer.module';
|
||||
export * from '@treo/components/drawer/drawer.service';
|
||||
export * from '@treo/components/drawer/drawer.types';
|
||||
@@ -0,0 +1,9 @@
|
||||
<ng-content></ng-content>
|
||||
|
||||
<!-- @formatter:off -->
|
||||
<ng-template let-highlightedCode="highlightedCode" let-lang="lang">
|
||||
<div class="treo-highlight treo-highlight-code-container">
|
||||
<pre [ngClass]="'language-' + lang"><code [ngClass]="'language-' + lang" [innerHTML]="highlightedCode"></code></pre>
|
||||
</div>
|
||||
</ng-template>
|
||||
<!-- @formatter:on -->
|
||||
@@ -0,0 +1,3 @@
|
||||
textarea[treo-highlight] {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EmbeddedViewRef, Input, Renderer2, SecurityContext, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { TreoHighlightService } from '@treo/components/highlight/highlight.service';
|
||||
|
||||
@Component({
|
||||
selector : 'textarea[treo-highlight]',
|
||||
templateUrl : './highlight.component.html',
|
||||
styleUrls : ['./highlight.component.scss'],
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs : 'treoHighlight'
|
||||
})
|
||||
export class TreoHighlightComponent implements AfterViewInit
|
||||
{
|
||||
highlightedCode: string;
|
||||
viewRef: EmbeddedViewRef<any>;
|
||||
|
||||
@ViewChild(TemplateRef)
|
||||
templateRef: TemplateRef<any>;
|
||||
|
||||
// Private
|
||||
private _code: string;
|
||||
private _lang: string;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoHighlightService} _treoHighlightService
|
||||
* @param {DomSanitizer} _domSanitizer
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
* @param {ElementRef} _elementRef
|
||||
* @param {Renderer2} _renderer2
|
||||
* @param {ViewContainerRef} _viewContainerRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoHighlightService: TreoHighlightService,
|
||||
private _domSanitizer: DomSanitizer,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _elementRef: ElementRef,
|
||||
private _renderer2: Renderer2,
|
||||
private _viewContainerRef: ViewContainerRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._code = '';
|
||||
this._lang = '';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter and getter for the code
|
||||
*/
|
||||
@Input()
|
||||
set code(value: string)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._code === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the code
|
||||
this._code = value;
|
||||
|
||||
// Highlight and insert the code if the
|
||||
// viewContainerRef is available. This will
|
||||
// ensure the highlightAndInsert method
|
||||
// won't run before the AfterContentInit hook.
|
||||
if ( this._viewContainerRef.length )
|
||||
{
|
||||
this._highlightAndInsert();
|
||||
}
|
||||
}
|
||||
|
||||
get code(): string
|
||||
{
|
||||
return this._code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for the language
|
||||
*/
|
||||
@Input()
|
||||
set lang(value: string)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._lang === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the language
|
||||
this._lang = value;
|
||||
|
||||
// Highlight and insert the code if the
|
||||
// viewContainerRef is available. This will
|
||||
// ensure the highlightAndInsert method
|
||||
// won't run before the AfterContentInit hook.
|
||||
if ( this._viewContainerRef.length )
|
||||
{
|
||||
this._highlightAndInsert();
|
||||
}
|
||||
}
|
||||
|
||||
get lang(): string
|
||||
{
|
||||
return this._lang;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* After view init
|
||||
*/
|
||||
ngAfterViewInit(): void
|
||||
{
|
||||
// Return, if there is no language set
|
||||
if ( !this.lang )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is no code input, get the code from
|
||||
// the textarea
|
||||
if ( !this.code )
|
||||
{
|
||||
// Get the code
|
||||
this.code = this._elementRef.nativeElement.value;
|
||||
}
|
||||
|
||||
// Highlight and insert
|
||||
this._highlightAndInsert();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Highlight and insert the highlighted code
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private _highlightAndInsert(): void
|
||||
{
|
||||
// Return, if the code or language is not defined
|
||||
if ( !this.code || !this.lang )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy the component if there is already one
|
||||
if ( this.viewRef )
|
||||
{
|
||||
this.viewRef.destroy();
|
||||
}
|
||||
|
||||
// Highlight and sanitize the code just in case
|
||||
this.highlightedCode = this._domSanitizer.sanitize(SecurityContext.HTML, this._treoHighlightService.highlight(this.code, this.lang));
|
||||
|
||||
// Render and insert the template
|
||||
this.viewRef = this._viewContainerRef.createEmbeddedView(this.templateRef, {
|
||||
highlightedCode: this.highlightedCode,
|
||||
lang : this.lang
|
||||
});
|
||||
|
||||
// Detect the changes
|
||||
this.viewRef.detectChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TreoHighlightComponent } from '@treo/components/highlight/highlight.component';
|
||||
|
||||
@NgModule({
|
||||
declarations : [
|
||||
TreoHighlightComponent
|
||||
],
|
||||
imports : [
|
||||
CommonModule
|
||||
],
|
||||
exports : [
|
||||
TreoHighlightComponent
|
||||
],
|
||||
entryComponents: [
|
||||
TreoHighlightComponent
|
||||
]
|
||||
})
|
||||
export class TreoHighlightModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as hljs from 'highlight.js';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TreoHighlightService
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Remove the empty lines around the code block
|
||||
* and re-align the indentation based on the first
|
||||
* non-whitespace indented character
|
||||
*
|
||||
* @param code
|
||||
* @private
|
||||
*/
|
||||
private _format(code: string): string
|
||||
{
|
||||
let firstCharIndentation: number | null = null;
|
||||
|
||||
// Split the code into lines and store the lines
|
||||
const lines = code.split('\n');
|
||||
|
||||
// Trim the empty lines around the code block
|
||||
while ( lines.length && lines[0].trim() === '' )
|
||||
{
|
||||
lines.shift();
|
||||
}
|
||||
|
||||
while ( lines.length && lines[lines.length - 1].trim() === '' )
|
||||
{
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
// Iterate through the lines to figure out the first
|
||||
// non-whitespace character indentation
|
||||
lines.forEach((line) => {
|
||||
|
||||
// Skip the line if its length is zero
|
||||
if ( line.length === 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// We look at all the lines to find the smallest indentation
|
||||
// of the first non-whitespace char since the first ever line
|
||||
// is not necessarily has to be the line with the smallest
|
||||
// non-whitespace char indentation
|
||||
firstCharIndentation = firstCharIndentation === null ?
|
||||
line.search(/\S|$/) :
|
||||
Math.min(line.search(/\S|$/), firstCharIndentation);
|
||||
});
|
||||
|
||||
// Iterate through the lines one more time, remove the extra
|
||||
// indentation, join them together and return it
|
||||
return lines.map((line) => {
|
||||
return line.substring(firstCharIndentation);
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Highlight
|
||||
*/
|
||||
highlight(code: string, language: string): string
|
||||
{
|
||||
// Format the code
|
||||
code = this._format(code);
|
||||
|
||||
// Highlight and return the code
|
||||
return hljs.highlight(language, code).value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@treo/components/highlight/public-api';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from '@treo/components/highlight/highlight.component';
|
||||
export * from '@treo/components/highlight/highlight.module';
|
||||
export * from '@treo/components/highlight/highlight.service';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@treo/components/message/public-api';
|
||||
@@ -0,0 +1,70 @@
|
||||
<div class="treo-message-container"
|
||||
*ngIf="!dismissed"
|
||||
[@fadeIn]="dismissed === null ? false : !dismissed"
|
||||
[@fadeOut]="dismissed === null ? false : !dismissed">
|
||||
|
||||
<!-- Icon -->
|
||||
<div class="treo-message-icon"
|
||||
*ngIf="showIcon">
|
||||
|
||||
<!-- Custom icon -->
|
||||
<div class="treo-message-custom-icon">
|
||||
<ng-content select="[treoMessageIcon]"></ng-content>
|
||||
</div>
|
||||
|
||||
<!-- Default icons -->
|
||||
<div class="treo-message-default-icon">
|
||||
|
||||
<mat-icon *ngIf="type === 'primary'"
|
||||
[svgIcon]="'check_circle'"></mat-icon>
|
||||
|
||||
<mat-icon *ngIf="type === 'accent'"
|
||||
[svgIcon]="'check_circle'"></mat-icon>
|
||||
|
||||
<mat-icon *ngIf="type === 'warn'"
|
||||
[svgIcon]="'warning'"></mat-icon>
|
||||
|
||||
<mat-icon *ngIf="type === 'basic'"
|
||||
[svgIcon]="'check_circle'"></mat-icon>
|
||||
|
||||
<mat-icon *ngIf="type === 'info'"
|
||||
[svgIcon]="'info'"></mat-icon>
|
||||
|
||||
<mat-icon *ngIf="type === 'success'"
|
||||
[svgIcon]="'check_circle'"></mat-icon>
|
||||
|
||||
<mat-icon *ngIf="type === 'warning'"
|
||||
[svgIcon]="'warning'"></mat-icon>
|
||||
|
||||
<mat-icon *ngIf="type === 'error'"
|
||||
[svgIcon]="'error'"></mat-icon>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="treo-message-content">
|
||||
|
||||
<div class="treo-message-title">
|
||||
<p>
|
||||
<ng-content select="[treoMessageTitle]"></ng-content>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="treo-message-message">
|
||||
<p>
|
||||
<ng-content></ng-content>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dismiss button -->
|
||||
<button class="treo-message-dismiss-button"
|
||||
mat-icon-button
|
||||
(click)="dismiss()">
|
||||
<mat-icon [svgIcon]="'close'"></mat-icon>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,592 @@
|
||||
@import 'treo';
|
||||
|
||||
treo-message {
|
||||
display: block;
|
||||
|
||||
// Show icon
|
||||
&.treo-message-show-icon {
|
||||
|
||||
.treo-message-container {
|
||||
padding-left: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
// Dismissible
|
||||
&.treo-message-dismissible {
|
||||
|
||||
.treo-message-container {
|
||||
padding-right: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
// Common
|
||||
.treo-message-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 64px;
|
||||
padding: 16px 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
// Icon
|
||||
.treo-message-icon {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 17px;
|
||||
|
||||
.treo-message-custom-icon,
|
||||
.treo-message-default-icon {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&:not(:empty) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.treo-message-custom-icon {
|
||||
display: none;
|
||||
|
||||
&:not(:empty) {
|
||||
display: flex;
|
||||
|
||||
+ .treo-message-default-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
.treo-message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
|
||||
// Title
|
||||
.treo-message-title {
|
||||
display: none;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
&:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.625;
|
||||
}
|
||||
}
|
||||
|
||||
// Message
|
||||
.treo-message-message {
|
||||
display: none;
|
||||
|
||||
&:not(:empty) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.625;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dismiss button
|
||||
.treo-message-dismiss-button {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 32px !important;
|
||||
min-width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
margin-left: auto;
|
||||
|
||||
.mat-icon {
|
||||
@include treo-icon-size(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dismissible
|
||||
&:not(.treo-message-dismissible) {
|
||||
|
||||
.treo-message-container {
|
||||
|
||||
.treo-message-dismiss-button {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Border
|
||||
&.treo-message-appearance-border {
|
||||
|
||||
.treo-message-container {
|
||||
overflow: hidden;
|
||||
border-left-width: 4px;
|
||||
border-radius: 4px;
|
||||
@include treo-elevation('xl');
|
||||
}
|
||||
}
|
||||
|
||||
// Fill
|
||||
&.treo-message-appearance-fill {
|
||||
|
||||
.treo-message-container {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// Outline
|
||||
&.treo-message-appearance-outline {
|
||||
|
||||
.treo-message-container {
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, warn);
|
||||
$is-dark: map-get($theme, is-dark);
|
||||
|
||||
treo-message {
|
||||
|
||||
.treo-message-container {
|
||||
|
||||
// Icon
|
||||
.mat-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Border
|
||||
&.treo-message-appearance-border {
|
||||
|
||||
.treo-message-container {
|
||||
background: map-get($background, card);
|
||||
|
||||
.treo-message-message {
|
||||
color: map-get($foreground, secondary-text);
|
||||
}
|
||||
}
|
||||
|
||||
// Primary
|
||||
&.treo-message-type-primary {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: map-get($primary, default);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: map-get($primary, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accent
|
||||
&.treo-message-type-accent {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: map-get($accent, default);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: map-get($accent, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warn
|
||||
&.treo-message-type-warn {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: map-get($warn, default);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: map-get($warn, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic
|
||||
&.treo-message-type-basic {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: treo-color('cool-gray', 600);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: treo-color('cool-gray', 600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
&.treo-message-type-info {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: treo-color('blue', 600);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: treo-color('blue', 700);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success
|
||||
&.treo-message-type-success {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: treo-color('green', 500);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: treo-color('green', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warning
|
||||
&.treo-message-type-warning {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: treo-color('yellow', 400);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: treo-color('yellow', 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
&.treo-message-type-error {
|
||||
|
||||
.treo-message-container {
|
||||
border-left-color: treo-color('red', 600);
|
||||
|
||||
.treo-message-title,
|
||||
.treo-message-icon {
|
||||
color: treo-color('red', 700);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill
|
||||
&.treo-message-appearance-fill {
|
||||
|
||||
// Primary
|
||||
&.treo-message-type-primary {
|
||||
|
||||
.treo-message-container {
|
||||
background: map-get($primary, default);
|
||||
color: map-get($primary, default-contrast);
|
||||
|
||||
code {
|
||||
background: map-get($primary, 600);
|
||||
color: map-get($primary, '600-contrast');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accent
|
||||
&.treo-message-type-accent {
|
||||
|
||||
.treo-message-container {
|
||||
background: map-get($accent, default);
|
||||
color: map-get($accent, default-contrast);
|
||||
|
||||
code {
|
||||
background: map-get($accent, 600);
|
||||
color: map-get($accent, '600-contrast');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warn
|
||||
&.treo-message-type-warn {
|
||||
|
||||
.treo-message-container {
|
||||
background: map-get($warn, default);
|
||||
color: map-get($warn, default-contrast);
|
||||
|
||||
code {
|
||||
background: map-get($warn, 800);
|
||||
color: map-get($warn, '800-contrast');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic
|
||||
&.treo-message-type-basic {
|
||||
|
||||
.treo-message-container {
|
||||
background: treo-color('cool-gray', 500);
|
||||
color: treo-color('cool-gray', 50);
|
||||
|
||||
code {
|
||||
background: treo-color('cool-gray', 600);
|
||||
color: treo-color('cool-gray', 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
&.treo-message-type-info {
|
||||
|
||||
.treo-message-container {
|
||||
background: treo-color('blue', 600);
|
||||
color: treo-color('blue', 50);
|
||||
|
||||
code {
|
||||
background: treo-color('blue', 800);
|
||||
color: treo-color('blue', 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success
|
||||
&.treo-message-type-success {
|
||||
|
||||
.treo-message-container {
|
||||
background: treo-color('green', 500);
|
||||
color: treo-color('green', 50);
|
||||
|
||||
code {
|
||||
background: treo-color('green', 600);
|
||||
color: treo-color('green', 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warning
|
||||
&.treo-message-type-warning {
|
||||
|
||||
.treo-message-container {
|
||||
background: treo-color('yellow', 400);
|
||||
color: treo-color('yellow', 50);
|
||||
|
||||
code {
|
||||
background: treo-color('yellow', 600);
|
||||
color: treo-color('yellow', 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
&.treo-message-type-error {
|
||||
|
||||
.treo-message-container {
|
||||
background: treo-color('red', 600);
|
||||
color: treo-color('red', 50);
|
||||
|
||||
code {
|
||||
background: treo-color('red', 800);
|
||||
color: treo-color('red', 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Outline
|
||||
&.treo-message-appearance-outline {
|
||||
|
||||
// Primary
|
||||
&.treo-message-type-primary {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: map-get($primary, 300);
|
||||
box-shadow: inset 0 0 0 1px map-get($primary, 300);
|
||||
} @else {
|
||||
background: map-get($primary, 50);
|
||||
color: map-get($primary, 800);
|
||||
box-shadow: inset 0 0 0 1px map-get($primary, 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: map-get($primary, 200);
|
||||
color: map-get($primary, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accent
|
||||
&.treo-message-type-accent {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: map-get($accent, 300);
|
||||
box-shadow: inset 0 0 0 1px map-get($accent, 300);
|
||||
} @else {
|
||||
background: map-get($accent, 50);
|
||||
color: map-get($accent, 800);
|
||||
box-shadow: inset 0 0 0 1px map-get($accent, 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: map-get($accent, 200);
|
||||
color: map-get($accent, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warn
|
||||
&.treo-message-type-warn {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: map-get($warn, 300);
|
||||
box-shadow: inset 0 0 0 1px map-get($warn, 300);
|
||||
} @else {
|
||||
background: map-get($warn, 50);
|
||||
color: map-get($warn, 800);
|
||||
box-shadow: inset 0 0 0 1px map-get($warn, 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: map-get($warn, 200);
|
||||
color: map-get($warn, 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic
|
||||
&.treo-message-type-basic {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: treo-color('cool-gray', 300);
|
||||
box-shadow: inset 0 0 0 1px treo-color('cool-gray', 300);
|
||||
} @else {
|
||||
background: treo-color('cool-gray', 50);
|
||||
color: treo-color('cool-gray', 800);
|
||||
box-shadow: inset 0 0 0 1px treo-color('cool-gray', 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: treo-color('cool-gray', 200);
|
||||
color: treo-color('cool-gray', 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
&.treo-message-type-info {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: treo-color('blue', 300);
|
||||
box-shadow: inset 0 0 0 1px treo-color('blue', 300);
|
||||
} @else {
|
||||
background: treo-color('blue', 50);
|
||||
color: treo-color('blue', 800);
|
||||
box-shadow: inset 0 0 0 1px treo-color('blue', 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: treo-color('blue', 200);
|
||||
color: treo-color('blue', 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success
|
||||
&.treo-message-type-success {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: treo-color('green', 300);
|
||||
box-shadow: inset 0 0 0 1px treo-color('green', 300);
|
||||
} @else {
|
||||
background: treo-color('green', 50);
|
||||
color: treo-color('green', 800);
|
||||
box-shadow: inset 0 0 0 1px treo-color('green', 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: treo-color('green', 200);
|
||||
color: treo-color('green', 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warning
|
||||
&.treo-message-type-warning {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: treo-color('yellow', 300);
|
||||
box-shadow: inset 0 0 0 1px treo-color('yellow', 300);
|
||||
} @else {
|
||||
background: treo-color('yellow', 50);
|
||||
color: treo-color('yellow', 800);
|
||||
box-shadow: inset 0 0 0 1px treo-color('yellow', 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: treo-color('yellow', 200);
|
||||
color: treo-color('yellow', 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
&.treo-message-type-error {
|
||||
|
||||
.treo-message-container {
|
||||
@if ($is-dark) {
|
||||
background: transparent;
|
||||
color: treo-color('red', 500);
|
||||
box-shadow: inset 0 0 0 1px treo-color('red', 500);
|
||||
} @else {
|
||||
background: treo-color('red', 50);
|
||||
color: treo-color('red', 800);
|
||||
box-shadow: inset 0 0 0 1px treo-color('red', 400);
|
||||
}
|
||||
|
||||
code {
|
||||
background: treo-color('red', 200);
|
||||
color: treo-color('red', 800);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, Renderer2, ViewEncapsulation } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
import { TreoAnimations } from '@treo/animations';
|
||||
import { TreoMessageAppearance, TreoMessageType } from '@treo/components/message/message.types';
|
||||
import { TreoMessageService } from '@treo/components/message/message.service';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-message',
|
||||
templateUrl : './message.component.html',
|
||||
styleUrls : ['./message.component.scss'],
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations : TreoAnimations,
|
||||
exportAs : 'treoMessage'
|
||||
})
|
||||
export class TreoMessageComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
@Output()
|
||||
readonly afterDismissed: EventEmitter<boolean>;
|
||||
|
||||
@Output()
|
||||
readonly afterShown: EventEmitter<boolean>;
|
||||
|
||||
// Private
|
||||
private _appearance: TreoMessageAppearance;
|
||||
private _dismissed: null | boolean;
|
||||
private _showIcon: boolean;
|
||||
private _type: TreoMessageType;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoMessageService} _treoMessageService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
* @param {ElementRef} _elementRef
|
||||
* @param {Renderer2} _renderer2
|
||||
*/
|
||||
constructor(
|
||||
private _treoMessageService: TreoMessageService,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _elementRef: ElementRef,
|
||||
private _renderer2: Renderer2
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.afterDismissed = new EventEmitter<boolean>();
|
||||
this.afterShown = new EventEmitter<boolean>();
|
||||
this.appearance = 'fill';
|
||||
this.dismissed = null;
|
||||
this.showIcon = true;
|
||||
this.type = 'primary';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter and getter for appearance
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set appearance(value: TreoMessageAppearance)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._appearance === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the class name
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-appearance-' + this.appearance);
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-appearance-' + value);
|
||||
|
||||
// Store the value
|
||||
this._appearance = value;
|
||||
}
|
||||
|
||||
get appearance(): TreoMessageAppearance
|
||||
{
|
||||
return this._appearance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for dismissed
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set dismissed(value: null | boolean)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._dismissed === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the class name
|
||||
if ( value === null )
|
||||
{
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-dismissible');
|
||||
}
|
||||
else if ( value === false )
|
||||
{
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissible');
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissed');
|
||||
}
|
||||
else
|
||||
{
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-dismissible');
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-dismissed');
|
||||
}
|
||||
|
||||
// Store the value
|
||||
this._dismissed = value;
|
||||
}
|
||||
|
||||
get dismissed(): null | boolean
|
||||
{
|
||||
return this._dismissed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for show icon
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set showIcon(value: boolean)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._showIcon === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the class name
|
||||
if ( value )
|
||||
{
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-show-icon');
|
||||
}
|
||||
else
|
||||
{
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-show-icon');
|
||||
}
|
||||
|
||||
// Store the value
|
||||
this._showIcon = value;
|
||||
}
|
||||
|
||||
get showIcon(): boolean
|
||||
{
|
||||
return this._showIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for type
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Input()
|
||||
set type(value: TreoMessageType)
|
||||
{
|
||||
// If the value is the same, return...
|
||||
if ( this._type === value )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the class name
|
||||
this._renderer2.removeClass(this._elementRef.nativeElement, 'treo-message-type-' + this.type);
|
||||
this._renderer2.addClass(this._elementRef.nativeElement, 'treo-message-type-' + value);
|
||||
|
||||
// Store the value
|
||||
this._type = value;
|
||||
}
|
||||
|
||||
get type(): TreoMessageType
|
||||
{
|
||||
return this._type;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Subscribe to the service calls if only
|
||||
// a name provided for the message box
|
||||
if ( this.name )
|
||||
{
|
||||
// Subscribe to the dismiss calls
|
||||
this._treoMessageService.onDismiss
|
||||
.pipe(
|
||||
filter((name) => this.name === name),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe(() => {
|
||||
|
||||
// Dismiss the message box
|
||||
this.dismiss();
|
||||
});
|
||||
|
||||
// Subscribe to the show calls
|
||||
this._treoMessageService.onShow
|
||||
.pipe(
|
||||
filter((name) => this.name === name),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe(() => {
|
||||
|
||||
// Show the message box
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Dismiss the message box
|
||||
*/
|
||||
dismiss(): void
|
||||
{
|
||||
// Return, if already dismissed
|
||||
if ( this.dismissed )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Dismiss
|
||||
this.dismissed = true;
|
||||
|
||||
// Execute the observable
|
||||
this.afterDismissed.next(true);
|
||||
|
||||
// Notify the change detector
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the dismissed message box
|
||||
*/
|
||||
show(): void
|
||||
{
|
||||
// Return, if not dismissed
|
||||
if ( !this.dismissed )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Show
|
||||
this.dismissed = false;
|
||||
|
||||
// Execute the observable
|
||||
this.afterShown.next(true);
|
||||
|
||||
// Notify the change detector
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TreoMessageComponent } from '@treo/components/message/message.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TreoMessageComponent
|
||||
],
|
||||
imports : [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatIconModule
|
||||
],
|
||||
exports : [
|
||||
TreoMessageComponent
|
||||
]
|
||||
})
|
||||
export class TreoMessageModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TreoMessageService
|
||||
{
|
||||
// Private
|
||||
private _onDismiss: BehaviorSubject<any>;
|
||||
private _onShow: BehaviorSubject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
// Set the private defaults
|
||||
this._onDismiss = new BehaviorSubject(null);
|
||||
this._onShow = new BehaviorSubject(null);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Getter for onDismiss
|
||||
*/
|
||||
get onDismiss(): Observable<any>
|
||||
{
|
||||
return this._onDismiss.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for onShow
|
||||
*/
|
||||
get onShow(): Observable<any>
|
||||
{
|
||||
return this._onShow.asObservable();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Dismiss the message box
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
dismiss(name: string): void
|
||||
{
|
||||
// Return, if the name is not provided
|
||||
if ( !name )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this._onDismiss.next(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the dismissed message box
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
show(name: string): void
|
||||
{
|
||||
// Return, if the name is not provided
|
||||
if ( !name )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the observable
|
||||
this._onShow.next(name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type TreoMessageAppearance = 'border' | 'fill' | 'outline';
|
||||
export type TreoMessageType = 'primary' | 'accent' | 'warn' | 'basic' | 'info' | 'success' | 'warning' | 'error';
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from '@treo/components/message/message.component';
|
||||
export * from '@treo/components/message/message.module';
|
||||
export * from '@treo/components/message/message.service';
|
||||
export * from '@treo/components/message/message.types';
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<!-- Item wrapper -->
|
||||
<div class="treo-horizontal-navigation-item-wrapper"
|
||||
[class.treo-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes">
|
||||
|
||||
<!-- Item with an internal link -->
|
||||
<div class="treo-horizontal-navigation-item"
|
||||
*ngIf="item.link && !item.externalLink && !item.function && !item.disabled"
|
||||
[routerLink]="[item.link]"
|
||||
[routerLinkActive]="'treo-horizontal-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="{exact: item.exactMatch || false}">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Item with an external link -->
|
||||
<a class="treo-horizontal-navigation-item"
|
||||
*ngIf="item.link && item.externalLink && !item.function && !item.disabled"
|
||||
[href]="item.link">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
|
||||
<!-- Item with a function -->
|
||||
<div class="treo-horizontal-navigation-item"
|
||||
*ngIf="!item.link && item.function && !item.disabled"
|
||||
(click)="item.function(item)">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Item with an internal link and function -->
|
||||
<div class="treo-horizontal-navigation-item"
|
||||
*ngIf="item.link && !item.externalLink && item.function && !item.disabled"
|
||||
[routerLink]="[item.link]"
|
||||
[routerLinkActive]="'treo-horizontal-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="{exact: item.exactMatch || false}"
|
||||
(click)="item.function(item)">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Item with an external link and function -->
|
||||
<a class="treo-horizontal-navigation-item"
|
||||
*ngIf="item.link && item.externalLink && item.function && !item.disabled"
|
||||
[href]="item.link"
|
||||
(click)="item.function(item)"
|
||||
mat-menu-item>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
|
||||
<!-- Item with a no link and no function -->
|
||||
<div class="treo-horizontal-navigation-item"
|
||||
*ngIf="!item.link && !item.function && !item.disabled">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Item is disabled -->
|
||||
<div class="treo-horizontal-navigation-item treo-horizontal-navigation-item-disabled"
|
||||
*ngIf="item.disabled">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Item template -->
|
||||
<ng-template #itemTemplate>
|
||||
|
||||
<!-- Icon -->
|
||||
<mat-icon class="treo-horizontal-navigation-item-icon"
|
||||
[ngClass]="item.iconClasses"
|
||||
*ngIf="item.icon"
|
||||
[svgIcon]="item.icon"></mat-icon>
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="treo-horizontal-navigation-item-title-wrapper">
|
||||
<div class="treo-horizontal-navigation-item-title">{{item.title}}</div>
|
||||
<div class="treo-horizontal-navigation-item-subtitle text-hint"
|
||||
*ngIf="item.subtitle">
|
||||
{{item.subtitle}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
<div class="treo-horizontal-navigation-item-badge"
|
||||
*ngIf="item.badge">
|
||||
<div class="treo-horizontal-navigation-item-badge-content"
|
||||
[ngClass]="[(item.badge.style != undefined ? 'treo-horizontal-navigation-item-badge-style-' + item.badge.style : ''),
|
||||
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
|
||||
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
|
||||
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
|
||||
{{item.badge.title}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-horizontal-navigation-basic-item',
|
||||
templateUrl : './basic.component.html',
|
||||
styles : [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoHorizontalNavigationBasicItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<div *ngIf="!child"
|
||||
[ngClass]="{'treo-horizontal-navigation-menu-active': trigger.menuOpen}"
|
||||
[matMenuTriggerFor]="matMenu"
|
||||
(onMenuOpen)="triggerChangeDetection()"
|
||||
(onMenuClose)="triggerChangeDetection()"
|
||||
#trigger="matMenuTrigger">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: item}"></ng-container>
|
||||
</div>
|
||||
|
||||
<mat-menu class="treo-horizontal-navigation-menu-panel"
|
||||
[overlapTrigger]="false"
|
||||
#matMenu="matMenu">
|
||||
|
||||
<ng-container *ngFor="let item of item.children">
|
||||
|
||||
<!-- Skip the hidden items -->
|
||||
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
|
||||
|
||||
<!-- Basic -->
|
||||
<div class="treo-horizontal-navigation-menu-item"
|
||||
*ngIf="item.type === 'basic'"
|
||||
mat-menu-item>
|
||||
<treo-horizontal-navigation-basic-item [item]="item"
|
||||
[name]="name"></treo-horizontal-navigation-basic-item>
|
||||
</div>
|
||||
|
||||
<!-- Branch: aside, collapsable, group -->
|
||||
<div class="treo-horizontal-navigation-menu-item"
|
||||
*ngIf="item.type === 'aside' || item.type === 'collapsable' || item.type === 'group'"
|
||||
[matMenuTriggerFor]="branch.matMenu"
|
||||
mat-menu-item>
|
||||
<ng-container *ngTemplateOutlet="itemTemplate; context: {$implicit: item}"></ng-container>
|
||||
<treo-horizontal-navigation-branch-item [child]="true"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
#branch></treo-horizontal-navigation-branch-item>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="treo-horizontal-navigation-menu-item"
|
||||
*ngIf="item.type === 'divider'"
|
||||
mat-menu-item>
|
||||
<treo-horizontal-navigation-divider-item [item]="item"
|
||||
[name]="name"></treo-horizontal-navigation-divider-item>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</mat-menu>
|
||||
|
||||
<!-- Item template -->
|
||||
<ng-template let-item
|
||||
#itemTemplate>
|
||||
|
||||
<div class="treo-horizontal-navigation-item-wrapper"
|
||||
[class.treo-horizontal-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes">
|
||||
|
||||
<div class="treo-horizontal-navigation-item"
|
||||
[ngClass]="{'treo-horizontal-navigation-item-disabled': item.disabled}">
|
||||
|
||||
<!-- Icon -->
|
||||
<mat-icon class="treo-horizontal-navigation-item-icon"
|
||||
[ngClass]="item.iconClasses"
|
||||
*ngIf="item.icon"
|
||||
[svgIcon]="item.icon"></mat-icon>
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="treo-horizontal-navigation-item-title-wrapper">
|
||||
<div class="treo-horizontal-navigation-item-title">{{item.title}}</div>
|
||||
<div class="treo-horizontal-navigation-item-subtitle text-hint"
|
||||
*ngIf="item.subtitle">
|
||||
{{item.subtitle}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
<div class="treo-horizontal-navigation-item-badge"
|
||||
*ngIf="item.badge">
|
||||
<div class="treo-horizontal-navigation-item-badge-content"
|
||||
[ngClass]="[(item.badge.style != undefined ? 'treo-horizontal-navigation-item-badge-style-' + item.badge.style : ''),
|
||||
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
|
||||
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
|
||||
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
|
||||
{{item.badge.title}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatMenu } from '@angular/material/menu';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-horizontal-navigation-branch-item',
|
||||
templateUrl : './branch.component.html',
|
||||
styles : [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoHorizontalNavigationBranchItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Child
|
||||
@Input()
|
||||
child: boolean;
|
||||
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Mat menu
|
||||
@ViewChild('matMenu', {static: true})
|
||||
matMenu: MatMenu;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.child = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Trigger the change detection
|
||||
*/
|
||||
triggerChangeDetection(): void
|
||||
{
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!-- Divider -->
|
||||
<div class="treo-horizontal-navigation-item-wrapper divider"></div>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-horizontal-navigation-divider-item',
|
||||
templateUrl : './divider.component.html',
|
||||
styles : [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoHorizontalNavigationDividerItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!-- Spacer -->
|
||||
<div class="treo-horizontal-navigation-item-wrapper"></div>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-horizontal-navigation-spacer-item',
|
||||
templateUrl : './spacer.component.html',
|
||||
styles : [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoHorizontalNavigationSpacerItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _treoHorizontalNavigationComponent: TreoHorizontalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoHorizontalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoHorizontalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="treo-horizontal-navigation-wrapper">
|
||||
|
||||
<ng-container *ngFor="let item of navigation">
|
||||
|
||||
<!-- Skip the hidden items -->
|
||||
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
|
||||
|
||||
<!-- Basic -->
|
||||
<treo-horizontal-navigation-basic-item class="treo-horizontal-navigation-menu-item"
|
||||
*ngIf="item.type === 'basic'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-horizontal-navigation-basic-item>
|
||||
|
||||
<!-- Branch: aside, collapsable, group -->
|
||||
<treo-horizontal-navigation-branch-item class="treo-horizontal-navigation-menu-item"
|
||||
*ngIf="item.type === 'aside' || item.type === 'collapsable' || item.type === 'group'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-horizontal-navigation-branch-item>
|
||||
|
||||
<!-- Spacer -->
|
||||
<treo-horizontal-navigation-spacer-item class="treo-horizontal-navigation-menu-item"
|
||||
*ngIf="item.type === 'spacer'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-horizontal-navigation-spacer-item>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,234 @@
|
||||
@import 'treo';
|
||||
|
||||
// Root navigation specific
|
||||
treo-horizontal-navigation {
|
||||
|
||||
.treo-horizontal-navigation-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// Basic, Branch
|
||||
treo-horizontal-navigation-basic-item,
|
||||
treo-horizontal-navigation-branch-item {
|
||||
|
||||
.treo-horizontal-navigation-item-wrapper {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.treo-horizontal-navigation-item {
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
.treo-horizontal-navigation-item-icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer
|
||||
treo-horizontal-navigation-spacer-item {
|
||||
margin: 12px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Menu panel specific
|
||||
.treo-horizontal-navigation-menu-panel {
|
||||
|
||||
.treo-horizontal-navigation-menu-item {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
|
||||
// Basic, Branch
|
||||
treo-horizontal-navigation-basic-item,
|
||||
treo-horizontal-navigation-branch-item,
|
||||
treo-horizontal-navigation-divider-item {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
// Divider
|
||||
treo-horizontal-navigation-divider-item {
|
||||
margin: 8px -16px;
|
||||
|
||||
.treo-horizontal-navigation-item-wrapper {
|
||||
height: 1px;
|
||||
box-shadow: 0 1px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation menu item common
|
||||
.treo-horizontal-navigation-menu-item {
|
||||
|
||||
.treo-horizontal-navigation-item-wrapper {
|
||||
width: 100%;
|
||||
|
||||
&.treo-horizontal-navigation-item-has-subtitle {
|
||||
|
||||
.treo-horizontal-navigation-item {
|
||||
min-height: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.treo-horizontal-navigation-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: 48px;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
|
||||
.treo-horizontal-navigation-item-title-wrapper {
|
||||
|
||||
.treo-horizontal-navigation-item-subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.treo-horizontal-navigation-item-badge {
|
||||
margin-left: auto;
|
||||
|
||||
.treo-horizontal-navigation-item-badge-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
|
||||
// Rectangle
|
||||
&.treo-horizontal-navigation-item-badge-style-rectangle {
|
||||
width: auto;
|
||||
min-width: 24px;
|
||||
height: 20px;
|
||||
line-height: normal;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// Rounded
|
||||
&.treo-horizontal-navigation-item-badge-style-rounded {
|
||||
width: auto;
|
||||
min-width: 24px;
|
||||
height: 20px;
|
||||
line-height: normal;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
// Simple
|
||||
&.treo-horizontal-navigation-item-badge-style-simple {
|
||||
width: auto;
|
||||
font-size: 11px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Theming
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
@include treo-theme {
|
||||
|
||||
$background: map-get($theme, background);
|
||||
$primary: map-get($theme, primary);
|
||||
$is-dark: map-get($theme, is-dark);
|
||||
|
||||
// Root navigation specific
|
||||
treo-horizontal-navigation {
|
||||
|
||||
.treo-horizontal-navigation-wrapper {
|
||||
|
||||
// Basic, Branch
|
||||
treo-horizontal-navigation-basic-item,
|
||||
treo-horizontal-navigation-branch-item {
|
||||
|
||||
@include treo-breakpoint('gt-xs') {
|
||||
|
||||
&:hover {
|
||||
|
||||
.treo-horizontal-navigation-item-wrapper {
|
||||
background: map-get($background, hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic - When item active (current link)
|
||||
treo-horizontal-navigation-basic-item {
|
||||
|
||||
.treo-horizontal-navigation-item-active {
|
||||
|
||||
.treo-horizontal-navigation-item-title {
|
||||
color: map-get($primary, default) !important;
|
||||
}
|
||||
|
||||
.treo-horizontal-navigation-item-subtitle {
|
||||
@if ($is-dark) {
|
||||
color: map-get($primary, 600) !important;
|
||||
} @else {
|
||||
color: map-get($primary, 400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.treo-horizontal-navigation-item-icon {
|
||||
color: map-get($primary, default) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Branch - When menu open
|
||||
treo-horizontal-navigation-branch-item {
|
||||
|
||||
.treo-horizontal-navigation-menu-active {
|
||||
|
||||
.treo-horizontal-navigation-item-wrapper {
|
||||
background: map-get($background, hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation menu item common
|
||||
.treo-horizontal-navigation-menu-item {
|
||||
|
||||
// Basic - When item active (current link)
|
||||
treo-horizontal-navigation-basic-item {
|
||||
|
||||
.treo-horizontal-navigation-item-active {
|
||||
|
||||
.treo-horizontal-navigation-item-title {
|
||||
color: map-get($primary, default) !important;
|
||||
}
|
||||
|
||||
.treo-horizontal-navigation-item-subtitle {
|
||||
@if ($is-dark) {
|
||||
color: map-get($primary, 600) !important;
|
||||
} @else {
|
||||
color: map-get($primary, 400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.treo-horizontal-navigation-item-icon {
|
||||
color: map-get($primary, default) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { TreoAnimations } from '@treo/animations';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-horizontal-navigation',
|
||||
templateUrl : './horizontal.component.html',
|
||||
styleUrls : ['./horizontal.component.scss'],
|
||||
animations : TreoAnimations,
|
||||
encapsulation : ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs : 'treoHorizontalNavigation'
|
||||
})
|
||||
export class TreoHorizontalNavigationComponent implements OnInit, OnDestroy
|
||||
{
|
||||
onRefreshed: BehaviorSubject<boolean | null>;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _navigation: TreoNavigationItem[];
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.onRefreshed = new BehaviorSubject(null);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Accessors
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Setter & getter for data
|
||||
*/
|
||||
@Input()
|
||||
set navigation(value: TreoNavigationItem[])
|
||||
{
|
||||
// Store the data
|
||||
this._navigation = value;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
get navigation(): TreoNavigationItem[]
|
||||
{
|
||||
return this._navigation;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Register the navigation component
|
||||
this._treoNavigationService.registerComponent(this.name, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Deregister the navigation component from the registry
|
||||
this._treoNavigationService.deregisterComponent(this.name);
|
||||
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Refresh the component to apply the changes
|
||||
*/
|
||||
refresh(): void
|
||||
{
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Execute the observable
|
||||
this.onRefreshed.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@treo/components/navigation/public-api';
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TreoScrollbarModule } from '@treo/directives/scrollbar/public-api';
|
||||
import { TreoHorizontalNavigationBasicItemComponent } from '@treo/components/navigation/horizontal/components/basic/basic.component';
|
||||
import { TreoHorizontalNavigationBranchItemComponent } from '@treo/components/navigation/horizontal/components/branch/branch.component';
|
||||
import { TreoHorizontalNavigationDividerItemComponent } from '@treo/components/navigation/horizontal/components/divider/divider.component';
|
||||
import { TreoHorizontalNavigationSpacerItemComponent } from '@treo/components/navigation/horizontal/components/spacer/spacer.component';
|
||||
import { TreoHorizontalNavigationComponent } from '@treo/components/navigation/horizontal/horizontal.component';
|
||||
import { TreoVerticalNavigationAsideItemComponent } from '@treo/components/navigation/vertical/components/aside/aside.component';
|
||||
import { TreoVerticalNavigationBasicItemComponent } from '@treo/components/navigation/vertical/components/basic/basic.component';
|
||||
import { TreoVerticalNavigationCollapsableItemComponent } from '@treo/components/navigation/vertical/components/collapsable/collapsable.component';
|
||||
import { TreoVerticalNavigationDividerItemComponent } from '@treo/components/navigation/vertical/components/divider/divider.component';
|
||||
import { TreoVerticalNavigationGroupItemComponent } from '@treo/components/navigation/vertical/components/group/group.component';
|
||||
import { TreoVerticalNavigationSpacerItemComponent } from '@treo/components/navigation/vertical/components/spacer/spacer.component';
|
||||
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TreoHorizontalNavigationBasicItemComponent,
|
||||
TreoHorizontalNavigationBranchItemComponent,
|
||||
TreoHorizontalNavigationDividerItemComponent,
|
||||
TreoHorizontalNavigationSpacerItemComponent,
|
||||
TreoHorizontalNavigationComponent,
|
||||
TreoVerticalNavigationAsideItemComponent,
|
||||
TreoVerticalNavigationBasicItemComponent,
|
||||
TreoVerticalNavigationCollapsableItemComponent,
|
||||
TreoVerticalNavigationDividerItemComponent,
|
||||
TreoVerticalNavigationGroupItemComponent,
|
||||
TreoVerticalNavigationSpacerItemComponent,
|
||||
TreoVerticalNavigationComponent
|
||||
],
|
||||
imports : [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatTooltipModule,
|
||||
TreoScrollbarModule
|
||||
],
|
||||
exports : [
|
||||
TreoHorizontalNavigationComponent,
|
||||
TreoVerticalNavigationComponent
|
||||
]
|
||||
})
|
||||
export class TreoNavigationModule
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TreoNavigationService
|
||||
{
|
||||
// Private
|
||||
private _componentRegistry: Map<string, any>;
|
||||
private _navigationStore: Map<string, TreoNavigationItem[]>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
constructor()
|
||||
{
|
||||
// Set the private defaults
|
||||
this._componentRegistry = new Map<string, any>();
|
||||
this._navigationStore = new Map<string, any>();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register navigation component
|
||||
*
|
||||
* @param name
|
||||
* @param component
|
||||
*/
|
||||
registerComponent(name: string, component: any): void
|
||||
{
|
||||
this._componentRegistry.set(name, component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister navigation component
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
deregisterComponent(name: string): void
|
||||
{
|
||||
this._componentRegistry.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation component from the registry
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
getComponent(name: string): any
|
||||
{
|
||||
return this._componentRegistry.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the given navigation with the given key
|
||||
*
|
||||
* @param key
|
||||
* @param navigation
|
||||
*/
|
||||
storeNavigation(key: string, navigation: TreoNavigationItem[]): void
|
||||
{
|
||||
// Add to the store
|
||||
this._navigationStore.set(key, navigation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation from storage by key
|
||||
*
|
||||
* @param key
|
||||
* @returns {any}
|
||||
*/
|
||||
getNavigation(key: string): TreoNavigationItem[]
|
||||
{
|
||||
return this._navigationStore.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the navigation from the storage
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
deleteNavigation(key: string): void
|
||||
{
|
||||
// Check if the navigation exists
|
||||
if ( !this._navigationStore.has(key) )
|
||||
{
|
||||
console.warn(`Navigation with the key '${key}' does not exist in the store.`);
|
||||
}
|
||||
|
||||
// Delete from the storage
|
||||
this._navigationStore.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that returns a flattened
|
||||
* version of the given navigation array
|
||||
*
|
||||
* @param navigation
|
||||
* @param flatNavigation
|
||||
* @returns {TreoNavigationItem[]}
|
||||
*/
|
||||
getFlatNavigation(navigation: TreoNavigationItem[], flatNavigation: TreoNavigationItem[] = []): TreoNavigationItem[]
|
||||
{
|
||||
for ( const item of navigation )
|
||||
{
|
||||
if ( item.type === 'basic' )
|
||||
{
|
||||
flatNavigation.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( item.type === 'aside' || item.type === 'collapsable' || item.type === 'group' )
|
||||
{
|
||||
if ( item.children )
|
||||
{
|
||||
this.getFlatNavigation(item.children, flatNavigation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flatNavigation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that returns the item
|
||||
* with the given id from given navigation
|
||||
*
|
||||
* @param id
|
||||
* @param navigation
|
||||
*/
|
||||
getItem(id: string, navigation: TreoNavigationItem[]): TreoNavigationItem | null
|
||||
{
|
||||
for ( const item of navigation )
|
||||
{
|
||||
if ( item.id === id )
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
if ( item.children )
|
||||
{
|
||||
const childItem = this.getItem(id, item.children);
|
||||
|
||||
if ( childItem )
|
||||
{
|
||||
return childItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that returns the item's parent
|
||||
* with the given id from given navigation
|
||||
*
|
||||
* @param id
|
||||
* @param navigation
|
||||
* @param parent
|
||||
*/
|
||||
getItemParent(
|
||||
id: string,
|
||||
navigation: TreoNavigationItem[],
|
||||
parent: TreoNavigationItem[] | TreoNavigationItem
|
||||
): TreoNavigationItem[] | TreoNavigationItem | null
|
||||
{
|
||||
for ( const item of navigation )
|
||||
{
|
||||
if ( item.id === id )
|
||||
{
|
||||
return parent;
|
||||
}
|
||||
|
||||
if ( item.children )
|
||||
{
|
||||
const childItem = this.getItemParent(id, item.children, item);
|
||||
|
||||
if ( childItem )
|
||||
{
|
||||
return childItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export interface TreoNavigationItem
|
||||
{
|
||||
id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
type: 'aside' | 'basic' | 'collapsable' | 'divider' | 'group' | 'spacer';
|
||||
hidden?: (item: TreoNavigationItem) => boolean;
|
||||
disabled?: boolean;
|
||||
link?: string;
|
||||
externalLink?: boolean;
|
||||
exactMatch?: boolean;
|
||||
function?: (item: TreoNavigationItem) => void;
|
||||
classes?: string;
|
||||
icon?: string;
|
||||
iconClasses?: string;
|
||||
badge?: {
|
||||
title?: string;
|
||||
style?: 'rectangle' | 'rounded' | 'simple',
|
||||
background?: string;
|
||||
color?: string;
|
||||
};
|
||||
children?: TreoNavigationItem[];
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
export type TreoVerticalNavigationAppearance = string;
|
||||
export type TreoVerticalNavigationMode = 'over' | 'side';
|
||||
export type TreoVerticalNavigationPosition = 'left' | 'right';
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from '@treo/components/navigation/horizontal/horizontal.component';
|
||||
export * from '@treo/components/navigation/vertical/vertical.component';
|
||||
export * from '@treo/components/navigation/navigation.module';
|
||||
export * from '@treo/components/navigation/navigation.service';
|
||||
export * from '@treo/components/navigation/navigation.types';
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<div class="treo-vertical-navigation-item-wrapper"
|
||||
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes">
|
||||
|
||||
<div class="treo-vertical-navigation-item"
|
||||
[ngClass]="{'treo-vertical-navigation-item-active': active, 'treo-vertical-navigation-item-disabled': item.disabled}">
|
||||
|
||||
<!-- Icon -->
|
||||
<mat-icon class="treo-vertical-navigation-item-icon"
|
||||
[ngClass]="item.iconClasses"
|
||||
*ngIf="item.icon"
|
||||
[svgIcon]="item.icon"></mat-icon>
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="treo-vertical-navigation-item-title-wrapper">
|
||||
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
|
||||
<div class="treo-vertical-navigation-item-subtitle"
|
||||
*ngIf="item.subtitle">
|
||||
{{item.subtitle}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
<div class="treo-vertical-navigation-item-badge"
|
||||
*ngIf="item.badge">
|
||||
|
||||
<div class="treo-vertical-navigation-item-badge-content"
|
||||
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
|
||||
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
|
||||
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
|
||||
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
|
||||
{{item.badge.title}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!skipChildren">
|
||||
|
||||
<div class="treo-vertical-navigation-item-children">
|
||||
|
||||
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
|
||||
|
||||
<!-- Skip the hidden items -->
|
||||
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
|
||||
|
||||
<!-- Basic -->
|
||||
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-basic-item>
|
||||
|
||||
<!-- Collapsable -->
|
||||
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
|
||||
|
||||
<!-- Divider -->
|
||||
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-divider-item>
|
||||
|
||||
<!-- Group -->
|
||||
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-group-item>
|
||||
|
||||
<!-- Spacer -->
|
||||
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-spacer-item>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-vertical-navigation-aside-item',
|
||||
templateUrl : './aside.component.html',
|
||||
styles : [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoVerticalNavigationAsideItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Active
|
||||
@Input()
|
||||
active: boolean;
|
||||
|
||||
// Auto collapse
|
||||
@Input()
|
||||
autoCollapse: boolean;
|
||||
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Skip children
|
||||
@Input()
|
||||
skipChildren: boolean;
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.skipChildren = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoVerticalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<!-- Item wrapper -->
|
||||
<div class="treo-vertical-navigation-item-wrapper"
|
||||
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes">
|
||||
|
||||
<!-- Item with an internal link -->
|
||||
<a class="treo-vertical-navigation-item"
|
||||
*ngIf="item.link && !item.externalLink && !item.function && !item.disabled"
|
||||
[routerLink]="[item.link]"
|
||||
[routerLinkActive]="'treo-vertical-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="{exact: item.exactMatch || false}">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
|
||||
<!-- Item with an external link -->
|
||||
<a class="treo-vertical-navigation-item"
|
||||
*ngIf="item.link && item.externalLink && !item.function && !item.disabled"
|
||||
[href]="item.link">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
|
||||
<!-- Item with a function -->
|
||||
<div class="treo-vertical-navigation-item"
|
||||
*ngIf="!item.link && item.function && !item.disabled"
|
||||
(click)="item.function(item)">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Item with an internal link and function -->
|
||||
<a class="treo-vertical-navigation-item"
|
||||
*ngIf="item.link && !item.externalLink && item.function && !item.disabled"
|
||||
[routerLink]="[item.link]"
|
||||
[routerLinkActive]="'treo-vertical-navigation-item-active'"
|
||||
[routerLinkActiveOptions]="{exact: item.exactMatch || false}"
|
||||
(click)="item.function(item)">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
|
||||
<!-- Item with an external link and function -->
|
||||
<a class="treo-vertical-navigation-item"
|
||||
*ngIf="item.link && item.externalLink && item.function && !item.disabled"
|
||||
[href]="item.link"
|
||||
(click)="item.function(item)">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</a>
|
||||
|
||||
<!-- Item with a no link and no function -->
|
||||
<div class="treo-vertical-navigation-item"
|
||||
*ngIf="!item.link && !item.function && !item.disabled">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Item is disabled -->
|
||||
<div class="treo-vertical-navigation-item treo-vertical-navigation-item-disabled"
|
||||
*ngIf="item.disabled">
|
||||
<ng-container *ngTemplateOutlet="itemTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Item template -->
|
||||
<ng-template #itemTemplate>
|
||||
|
||||
<!-- Icon -->
|
||||
<mat-icon class="treo-vertical-navigation-item-icon"
|
||||
[ngClass]="item.iconClasses"
|
||||
*ngIf="item.icon"
|
||||
[svgIcon]="item.icon"></mat-icon>
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="treo-vertical-navigation-item-title-wrapper">
|
||||
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
|
||||
<div class="treo-vertical-navigation-item-subtitle"
|
||||
*ngIf="item.subtitle">
|
||||
{{item.subtitle}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
<div class="treo-vertical-navigation-item-badge"
|
||||
*ngIf="item.badge">
|
||||
<div class="treo-vertical-navigation-item-badge-content"
|
||||
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
|
||||
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
|
||||
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
|
||||
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
|
||||
{{item.badge.title}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-vertical-navigation-basic-item',
|
||||
templateUrl : './basic.component.html',
|
||||
styles : [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoVerticalNavigationBasicItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoVerticalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<div class="treo-vertical-navigation-item-wrapper"
|
||||
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes">
|
||||
|
||||
<div class="treo-vertical-navigation-item"
|
||||
[ngClass]="{'treo-vertical-navigation-item-disabled': item.disabled}"
|
||||
(click)="toggleCollapsable()">
|
||||
|
||||
<!-- Icon -->
|
||||
<mat-icon class="treo-vertical-navigation-item-icon"
|
||||
[ngClass]="item.iconClasses"
|
||||
*ngIf="item.icon"
|
||||
[svgIcon]="item.icon"></mat-icon>
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="treo-vertical-navigation-item-title-wrapper">
|
||||
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
|
||||
<div class="treo-vertical-navigation-item-subtitle"
|
||||
*ngIf="item.subtitle">
|
||||
{{item.subtitle}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
<div class="treo-vertical-navigation-item-badge"
|
||||
*ngIf="item.badge">
|
||||
|
||||
<div class="treo-vertical-navigation-item-badge-content"
|
||||
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
|
||||
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
|
||||
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
|
||||
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
|
||||
{{item.badge.title}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<mat-icon class="treo-vertical-navigation-item-arrow icon-size-16"
|
||||
[svgIcon]="'heroicons_solid:cheveron-right'"></mat-icon>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="treo-vertical-navigation-item-children"
|
||||
*ngIf="!isCollapsed"
|
||||
@expandCollapse>
|
||||
|
||||
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
|
||||
|
||||
<!-- Skip the hidden items -->
|
||||
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
|
||||
|
||||
<!-- Basic -->
|
||||
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-basic-item>
|
||||
|
||||
<!-- Collapsable -->
|
||||
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
|
||||
|
||||
<!-- Divider -->
|
||||
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-divider-item>
|
||||
|
||||
<!-- Group -->
|
||||
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-group-item>
|
||||
|
||||
<!-- Spacer -->
|
||||
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-spacer-item>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
import { TreoAnimations } from '@treo/animations';
|
||||
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-vertical-navigation-collapsable-item',
|
||||
templateUrl : './collapsable.component.html',
|
||||
styles : [],
|
||||
animations : TreoAnimations,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoVerticalNavigationCollapsableItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Auto collapse
|
||||
@Input()
|
||||
autoCollapse: boolean;
|
||||
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Collapsed
|
||||
@HostBinding('class.treo-vertical-navigation-item-collapsed')
|
||||
isCollapsed: boolean;
|
||||
|
||||
// Expanded
|
||||
@HostBinding('class.treo-vertical-navigation-item-expanded')
|
||||
isExpanded: boolean;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
* @param {Router} _router
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
private _router: Router
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
|
||||
// Set the defaults
|
||||
this.isCollapsed = true;
|
||||
this.isExpanded = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// If the item has a children that has a matching url with the current url, expand...
|
||||
if ( this._hasCurrentUrlInChildren(this.item, this._router.url) )
|
||||
{
|
||||
this.expand();
|
||||
}
|
||||
// Otherwise...
|
||||
else
|
||||
{
|
||||
// If the autoCollapse is on, collapse...
|
||||
if ( this.autoCollapse )
|
||||
{
|
||||
this.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for the onCollapsableItemCollapsed from the service
|
||||
this._treoVerticalNavigationComponent.onCollapsableItemCollapsed
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((collapsedItem) => {
|
||||
|
||||
// Check if the collapsed item is null
|
||||
if ( collapsedItem === null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Collapse if this is a children of the collapsed item
|
||||
if ( this._isChildrenOf(collapsedItem, this.item) )
|
||||
{
|
||||
this.collapse();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for the onCollapsableItemExpanded from the service if the autoCollapse is on
|
||||
if ( this.autoCollapse )
|
||||
{
|
||||
this._treoVerticalNavigationComponent.onCollapsableItemExpanded
|
||||
.pipe(takeUntil(this._unsubscribeAll))
|
||||
.subscribe((expandedItem) => {
|
||||
|
||||
// Check if the expanded item is null
|
||||
if ( expandedItem === null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a parent of the expanded item
|
||||
if ( this._isChildrenOf(this.item, expandedItem) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this has a children with a matching url with the current active url
|
||||
if ( this._hasCurrentUrlInChildren(this.item, this._router.url) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the expanded item
|
||||
if ( this.item === expandedItem )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If none of the above conditions are matched, collapse this item
|
||||
this.collapse();
|
||||
});
|
||||
}
|
||||
|
||||
// Attach a listener to the NavigationEnd event
|
||||
this._router.events
|
||||
.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
takeUntil(this._unsubscribeAll)
|
||||
)
|
||||
.subscribe((event: NavigationEnd) => {
|
||||
|
||||
// If the item has a children that has a matching url with the current url, expand...
|
||||
if ( this._hasCurrentUrlInChildren(this.item, event.urlAfterRedirects) )
|
||||
{
|
||||
this.expand();
|
||||
}
|
||||
// Otherwise...
|
||||
else
|
||||
{
|
||||
// If the autoCollapse is on, collapse...
|
||||
if ( this.autoCollapse )
|
||||
{
|
||||
this.collapse();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoVerticalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Private methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if the given item has the given url
|
||||
* in one of its children
|
||||
*
|
||||
* @param item
|
||||
* @param url
|
||||
* @private
|
||||
*/
|
||||
private _hasCurrentUrlInChildren(item, url): boolean
|
||||
{
|
||||
const children = item.children;
|
||||
|
||||
if ( !children )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for ( const child of children )
|
||||
{
|
||||
if ( child.children )
|
||||
{
|
||||
if ( this._hasCurrentUrlInChildren(child, url) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the item's link is the exact same of the
|
||||
// current url
|
||||
if ( child.link === url )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If exactMatch is not set for the item, also check
|
||||
// if the current url starts with the item's link and
|
||||
// continues with a question mark, a pound sign or a
|
||||
// slash
|
||||
if ( !child.exactMatch && (child.link === url || url.startsWith(child.link + '?') || url.startsWith(child.link + '#') || url.startsWith(child.link + '/')) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a children
|
||||
* of the given item
|
||||
*
|
||||
* @param parent
|
||||
* @param item
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
private _isChildrenOf(parent, item): boolean
|
||||
{
|
||||
const children = parent.children;
|
||||
|
||||
if ( !children )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( children.indexOf(item) > -1 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for ( const child of children )
|
||||
{
|
||||
if ( child.children )
|
||||
{
|
||||
if ( this._isChildrenOf(child, item) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Public methods
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Collapse
|
||||
*/
|
||||
collapse(): void
|
||||
{
|
||||
// Return if the item is disabled
|
||||
if ( this.item.disabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if the item is already collapsed
|
||||
if ( this.isCollapsed )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Collapse it
|
||||
this.isCollapsed = true;
|
||||
this.isExpanded = false;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Execute the observable
|
||||
this._treoVerticalNavigationComponent.onCollapsableItemCollapsed.next(this.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand
|
||||
*/
|
||||
expand(): void
|
||||
{
|
||||
// Return if the item is disabled
|
||||
if ( this.item.disabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if the item is already expanded
|
||||
if ( !this.isCollapsed )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand it
|
||||
this.isCollapsed = false;
|
||||
this.isExpanded = true;
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
|
||||
// Execute the observable
|
||||
this._treoVerticalNavigationComponent.onCollapsableItemExpanded.next(this.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle collapsable
|
||||
*/
|
||||
toggleCollapsable(): void
|
||||
{
|
||||
// Toggle collapse/expand
|
||||
if ( this.isCollapsed )
|
||||
{
|
||||
this.expand();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track by function for ngFor loops
|
||||
*
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
trackByFn(index: number, item: any): any
|
||||
{
|
||||
return item.id || index;
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!-- Divider -->
|
||||
<div class="treo-vertical-navigation-item-wrapper divider"></div>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { TreoVerticalNavigationComponent } from '@treo/components/navigation/vertical/vertical.component';
|
||||
import { TreoNavigationService } from '@treo/components/navigation/navigation.service';
|
||||
import { TreoNavigationItem } from '@treo/components/navigation/navigation.types';
|
||||
|
||||
@Component({
|
||||
selector : 'treo-vertical-navigation-divider-item',
|
||||
templateUrl : './divider.component.html',
|
||||
styles : [],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TreoVerticalNavigationDividerItemComponent implements OnInit, OnDestroy
|
||||
{
|
||||
// Item
|
||||
@Input()
|
||||
item: TreoNavigationItem;
|
||||
|
||||
// Name
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
// Private
|
||||
private _treoVerticalNavigationComponent: TreoVerticalNavigationComponent;
|
||||
private _unsubscribeAll: Subject<any>;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {TreoNavigationService} _treoNavigationService
|
||||
* @param {ChangeDetectorRef} _changeDetectorRef
|
||||
*/
|
||||
constructor(
|
||||
private _treoNavigationService: TreoNavigationService,
|
||||
private _changeDetectorRef: ChangeDetectorRef
|
||||
)
|
||||
{
|
||||
// Set the private defaults
|
||||
this._unsubscribeAll = new Subject();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
// @ Lifecycle hooks
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* On init
|
||||
*/
|
||||
ngOnInit(): void
|
||||
{
|
||||
// Get the parent navigation component
|
||||
this._treoVerticalNavigationComponent = this._treoNavigationService.getComponent(this.name);
|
||||
|
||||
// Subscribe to onRefreshed on the navigation component
|
||||
this._treoVerticalNavigationComponent.onRefreshed.pipe(
|
||||
takeUntil(this._unsubscribeAll)
|
||||
).subscribe(() => {
|
||||
|
||||
// Mark for check
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On destroy
|
||||
*/
|
||||
ngOnDestroy(): void
|
||||
{
|
||||
// Unsubscribe from all subscriptions
|
||||
this._unsubscribeAll.next();
|
||||
this._unsubscribeAll.complete();
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<!-- Item wrapper -->
|
||||
<div class="treo-vertical-navigation-item-wrapper"
|
||||
[class.treo-vertical-navigation-item-has-subtitle]="!!item.subtitle"
|
||||
[ngClass]="item.classes">
|
||||
|
||||
<div class="treo-vertical-navigation-item">
|
||||
|
||||
<!-- Icon -->
|
||||
<mat-icon class="treo-vertical-navigation-item-icon"
|
||||
[ngClass]="item.iconClasses"
|
||||
*ngIf="item.icon"
|
||||
[svgIcon]="item.icon"></mat-icon>
|
||||
|
||||
<!-- Title & Subtitle -->
|
||||
<div class="treo-vertical-navigation-item-title-wrapper">
|
||||
<div class="treo-vertical-navigation-item-title">{{item.title}}</div>
|
||||
<div class="treo-vertical-navigation-item-subtitle"
|
||||
*ngIf="item.subtitle">
|
||||
{{item.subtitle}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badge -->
|
||||
<div class="treo-vertical-navigation-item-badge"
|
||||
*ngIf="item.badge">
|
||||
|
||||
<div class="treo-vertical-navigation-item-badge-content"
|
||||
[ngClass]="[(item.badge.style != undefined ? 'treo-vertical-navigation-item-badge-style-' + item.badge.style : ''),
|
||||
(item.badge.background != undefined && !item.badge.background.startsWith('#') ? item.badge.background : ''),
|
||||
(item.badge.color != undefined && !item.badge.color.startsWith('#') ? item.badge.color : '')]"
|
||||
[ngStyle]="{'background-color': item.badge.background, 'color': item.badge.color}">
|
||||
{{item.badge.title}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-container *ngFor="let item of item.children; trackBy: trackByFn">
|
||||
|
||||
<!-- Skip the hidden items -->
|
||||
<ng-container *ngIf="(item.hidden && !item.hidden(item)) || !item.hidden">
|
||||
|
||||
<!-- Basic -->
|
||||
<treo-vertical-navigation-basic-item *ngIf="item.type === 'basic'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-basic-item>
|
||||
|
||||
<!-- Collapsable -->
|
||||
<treo-vertical-navigation-collapsable-item *ngIf="item.type === 'collapsable'"
|
||||
[item]="item"
|
||||
[name]="name"
|
||||
[autoCollapse]="autoCollapse"></treo-vertical-navigation-collapsable-item>
|
||||
|
||||
<!-- Divider -->
|
||||
<treo-vertical-navigation-divider-item *ngIf="item.type === 'divider'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-divider-item>
|
||||
|
||||
<!-- Group -->
|
||||
<treo-vertical-navigation-group-item *ngIf="item.type === 'group'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-group-item>
|
||||
|
||||
<!-- Spacer -->
|
||||
<treo-vertical-navigation-spacer-item *ngIf="item.type === 'spacer'"
|
||||
[item]="item"
|
||||
[name]="name"></treo-vertical-navigation-spacer-item>
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user