maestrano/mno-enterprise-angular

View on GitHub
src/app/components/theme-editor/theme-editor.directive.coffee

Summary

Maintainability
Test Coverage
ThemeEditorCtrl = ($scope, $log, $timeout,  toastr, themeEditorSvc) ->
  'ngInject'

  #============================================
  # Theme Config
  #============================================
  default_theme = {
    # Main colors
    '@bg-main-color':        "#aeb5bf"
    '@decorator-main-color': "#758192"
    '@decorator-alt-color':  "#977bf0"
    '@text-strong-color':    "#17262d"

    # Inverse colors
    '@bg-inverse-color':               "#ffffff"
    '@bg-on-bg-inverse-color':         "#cacfd6"
    '@bg-inverse-intense-color':       "#17262d"
    '@decorator-inverse-color':        "#977bf0"
    '@text-inverse-color':             "#17262d"

    # Color used in small/isolated elements
    '@elem-positive-color':            "#d1e55c"
    '@elem-positive-flash-color':      "#47ff00"
    '@elem-cozy-color':                "#977bf0"
  }

  $scope.theme = theme = angular.copy(default_theme)

  $scope.labels = {
    # Main colors
    '@bg-main-color':          "Dashboard background color"
    '@decorator-main-color':   "Elements placed on top of bg-main-color"
    '@decorator-alt-color':    "Elements placed on top of bg-main-color (secondary)"
    '@text-strong-color':      "Used for text used in the context of bg-main-color"
    # Inverse colors
    '@bg-inverse-color':       "Background color used for dashboard side menu. top banners, inverse modals"
    '@bg-on-bg-inverse-color': "Background color for elements displayed on top of inverse elements"
    '@bg-inverse-intense-color':       "Background color used for elements on top of bg-inverse-color"
    '@decorator-inverse-color':        "Design elements (lines, borders) on top of bg-inverse-color"
    '@text-inverse-color':             "Text used on top of bg-inverse-color such as inverse modals"

    # Color used in small/isolated elements
    '@elem-positive-color':            "used to reflect a positive action or point (e.g: tick icon)"
    '@elem-positive-flash-color':      "stronger version of elem-positive-color (barely used)"
    '@elem-cozy-color':                "used for regular design element - fits well with both main and inverse backgrounds"
  }

  # Length of longest key (for padding purpose)
  key_length = _.max(_.keys(default_theme), (k) -> k.length).length + 2


  default_variables = {
    'Fonts & Text': {
      '@font-family-sans-serif':           'Arial, "Helvetica Neue", Helvetica, sans-serif'
      '@font-family-base':                 '@font-family-sans-serif'
      '@small':                            '11px'
      '@normal':                           '13px'
    },

    'General Colors': {
      '@brand-success':                  '@elem-positive-color'
      '@brand-warning':                  '@decorator-alt-color'
      '@brand-info':                     '@text-inverse-color'
      '@brand-danger':                   '@text-inverse-color'
      '@brand-primary':                  '@text-inverse-color'
    },

    'Forms': {
      '@input-bg': '@elem-cozy-color'
      '@input-color': '@text-inverse-color'
      '@input-color-placeholder': '@input-color'

      '@input-label-color': '@text-inverse-color'
    }

    'Public Page Header': {
      '@public-page-header-bg-color':        '@bg-inverse-color'
      '@public-page-header-padding':         '10px'
      '@public-page-header-logo':            '{ min-height: 61px; max-width: 160px; max-height: 130px; margin: 17px auto 5px auto; }'
    },


    'Login Page': {
      '@login-bg-color':                     '@bg-main-color'
      '@login-box-bg-color':                 '@bg-inverse-color'
      '@login-box-padding':                  '20px'
      '@login-box-brand-logo':               '{ min-height: 61px; max-width: 160px; max-height: 130px; margin: 17px auto 5px auto; }'
      '@login-box-btn-login':                '{ width: 100%; }'
      '@login-box-grid-position':            '{ margin-top: 80px; .make-sm-column(4); .make-sm-column-offset(4); }'

      '@login-box-title-color':              '@decorator-main-color'
      '@login-box-title':                    '{ text-transform: uppercase; }'
      '@login-box-title-display-box-arrow':  'false'
    },

    'Dashboard Layout': {
      '@dashboard-bg-color':                            '@bg-main-color'

      '@dashboard-section-title-color':                 '@text-strong-color'
      '@dashboard-section-title-alignment':             'center'
      '@dashboard-section-title-display-subline':       'false'
    },

    'Dashboard Side Menu': {
      '@dashboard-side-menu-padding': '30px 0px 0px 14px'
      '@dashboard-side-menu-bg-color': '@bg-inverse-color'
      '@dashboard-side-menu-brand-logo': '{ background-size: 204px; width: 51px; margin-left: 5px; }'
      '@dashboard-side-menu-brand-logo-expanded': '{ width: 201px; background-size: 100%; }'

      '@dashboard-side-menu-link-color':               '@text-inverse-color'
      '@dashboard-side-menu-link-bg-color':            '@dashboard-side-menu-bg-color'
      '@dashboard-side-menu-link-hover-color':         '@dashboard-side-menu-bg-color'
      '@dashboard-side-menu-link-hover-bg-color':      'lighten(@bg-main-color,15%)'
      '@dashboard-side-menu-link-active-color':        '@dashboard-side-menu-bg-color'
      '@dashboard-side-menu-link-active-bg-color':     '@bg-main-color'
    },

    'Dashboard Company Select Box': {
      '@dashboard-cpy-select-bg-color':                 '@bg-inverse-color'
      '@dashboard-cpy-select-border-radius':            '0px'
      '@dashboard-cpy-select-padding':                  '15px 25px'

      '@dashboard-cpy-select-header-text-color':        '@text-inverse-color'
      '@dashboard-cpy-select-header-text-size':         '13px'

      '@dashboard-cpy-select-link-color':               '@dashboard-cpy-select-header-text-color'

      '@dashboard-cpy-select-link-hover-color':         '@dashboard-cpy-select-link-color'
      '@dashboard-cpy-select-link-hover-bg-color':      'lighten(@dashboard-cpy-select-bg-color,10%)'

      '@dashboard-cpy-select-link-create-color':        '@decorator-alt-color'
      '@dashboard-cpy-select-link-admin-color':         '@elem-positive-color'

      '@dashboard-cpy-select-switch-btn-color':         '@dashboard-cpy-select-header-text-color'
      '@dashboard-cpy-select-switch-btn-bg-color':      '@dashboard-cpy-select-bg-color'
    },

    'Dashboard Loader': {
      '@dashboard-loader-color':                        '@bg-inverse-color'
      '@dashboard-loader-icon':                         '{ .icon-fa(refresh); .fa-2x; .fa-spin; }'
    },

    'Apps Section': {
      '@dashboard-apps-tile-bg-color':                  '@bg-inverse-color'
      '@dashboard-apps-tile-text-color':                '@text-inverse-color'

      '@dashboard-apps-tile-logo-border-size':          '0px'
      '@dashboard-apps-tile-logo-border-color':         '@decorator-main-color'

      '@dashboard-apps-tile-settings-color':            '@dashboard-apps-tile-text-color'
      '@dashboard-apps-tile-settings-bg-color':         '@dashboard-apps-tile-bg-color'

      '@dashboard-apps-tile-settings-hover-color':      '@dashboard-apps-tile-bg-color'
      '@dashboard-apps-tile-settings-hover-bg-color':   '@dashboard-apps-tile-text-color'

      '@dashboard-apps-tile-add-color':                 '@dashboard-apps-tile-bg-color'
      '@dashboard-apps-tile-add-bg-color':              '@dashboard-apps-tile-bg-color'
    },

    'Company Section': {
      '@dashboard-cpy-tabs-bg-color':                    '@bg-inverse-color'
      '@dashboard-cpy-tabs-text-color':                  '@text-inverse-color'
      '@dashboard-cpy-tabs-border-radius':               '2px'

      '@dashboard-cpy-tabs-subline-size':                '0px'
      '@dashboard-cpy-tabs-subline-color':               'transparent'

      '@dashboard-cpy-tabs-active-text-color':           '@elem-positive-color'
      '@dashboard-cpy-tabs-active-bg-color':             '@bg-inverse-color'

      '@dashboard-cpy-tabs-hover-text-color':            '@elem-positive-color'
      '@dashboard-cpy-tabs-hover-bg-color':              '@bg-inverse-color'

      '@dashboard-cpy-tabcontent-bg-color':              '@dashboard-cpy-tabs-bg-color'
    },

    'Company Team Section': {
      '@dashboard-cpy-teams-matrix-bg-color':            '@bg-inverse-color'
      '@dashboard-cpy-teams-matrix-border-color':        'lighten(@bg-main-color,30%)'
    },

    'Impac Dashboard': {
      '@impac-dashboard-padding-top':                   '33px'
      '@impac-dashboard-margin-left':                   '0px'

      '@impac-dashboard-title-label-color':             '@text-strong-color'
      '@impac-dashboard-title-color':                   '@elem-cozy-color'
      '@impac-dashboard-source-color':                  '@impac-dashboard-title-label-color'
      '@impac-dashboard-buttons-border-radius':         '4px'

      '@impac-placeholder-border':                      '2px dashed @bg-inverse-color'
      '@impac-padding-between-widgets':                 '12px'
      '@impac-minimum-widget-size':                     '258px'
    },

    'Impac Widgets': {
      # Global
      '@impac-widget-background-color':                 '@bg-inverse-color'
      '@impac-widget-text-color':                       '@text-inverse-color'
      '@impac-widget-text-color-light':                 'lighten(@impac-widget-text-color,70%)'
      '@impac-widget-borders-color':                    'lighten(@impac-widget-text-color-light,10%)'
      '@impac-widget-link-color':                       '@decorator-inverse-color'

      # Title
      '@impac-widget-title-text-color':                 'darken(@impac-widget-text-color,5%)'
      '@impac-widget-title-bg':                         '@impac-widget-background-color'
      '@impac-widget-title-border':                     'solid 1px @impac-widget-borders-color'
      '@impac-widget-title-text-transform':             'uppercase'
      '@impac-widget-title-text-size':                  '12px'
      '@impac-widget-title-border-radius':              '5px 5px 0px 0px'

      # Content
      '@impac-widget-content-border-radius':            '0px 0px 5px 5px'
      '@impac-widget-lines-container-max-height':             '210px'

      # Hist Mode Choser
      '@impac-widget-hist-text-transform':              'uppercase'
      '@impac-widget-hist-text-size':                   '12px'
      '@impac-widget-hist-text-color':                  '@impac-widget-text-color-light'

      # Price
      '@impac-widget-price-color':                      '@impac-widget-text-color'
      '@impac-widget-price-positive-color':             '@elem-positive-color'
      '@impac-widget-price-negative-color':             '@elem-cozy-color'
      '@impac-widget-currency-color':                   '@impac-widget-text-color-light'
      '@impac-widget-legend-color':                     'lighten(@impac-widget-text-color,30%)'

      # Edit settings
      '@impac-widget-sub-bg-color':                     'darken(@impac-widget-background-color,10%)'
      '@impac-widget-sub-bg-color-light':               'lighten(@impac-widget-sub-bg-color,5%)'

      # Invoices list
      '@impac-widget-line-hover-bg':                    'lighten(@bg-inverse-color,10%)'
      '@impac-widget-line-hover-text':                  '@text-inverse-color'

      # Accounts Comparison
      '@impac-big-widget-size':                         '581px'
      '@impac-big-widget-bottom-padding':               '30px'
      '@impac-widget-accounts-comparison-lines-container-max-height': '250px'
    },

    'Marketplace Section': {
      '@dashboard-marketplace-search-text-color':             '@bg-inverse-color'
      '@dashboard-marketplace-search-border-color':           '@bg-inverse-intense-color'

      '@dashboard-marketplace-tile-bg-color':                 '@bg-inverse-color'
      '@dashboard-marketplace-tile-text-color':               '@text-inverse-color'
      '@dashboard-marketplace-tile-img-border-color':         '@dashboard-marketplace-tile-bg-color'

      '@dashboard-marketplace-tile-hover-bg-color':           '@decorator-main-color'
      '@dashboard-marketplace-tile-hover-text-color':         '@dashboard-marketplace-tile-text-color'
      '@dashboard-marketplace-tile-hover-img-border-color':   'darken(@decorator-main-color,10%)'
      '@dashboard-marketplace-tile-hover-arrow-color':        '@text-strong-color'

      '@dashboard-marketplace-show-header-text-color':        '@bg-inverse-color'

      '@dashboard-marketplace-app-card':                      '{ display: block; padding: 10px; height: 120px; margin-bottom: 10px; font-weight: 300; }'
    },
  }
  $scope.variables = variables = angular.copy(default_variables)

  #============================================
  # View methods
  #============================================
  $scope.editor = editor = {busy: false, output: ''}

  editor.update = () ->
    return true if editor.updating
    editor.busy = true
    vars = mergeLessVars()

    # Apply style
    less.modifyVars(vars).then ->
      editor.busy = false
      $scope.$apply()

  editor.reset = (opts = {published:false}) ->
    editor.busy = true
    if opts.published
      themeEditorSvc.resetToPublishedTheme()
        .then(-> loadLastSavedTheme())
        .then(-> editor.update())
    else
      $scope.theme = theme = angular.copy(default_theme)
      $scope.variables = variables = angular.copy(default_variables)
      editor.update()

  editor.local_save = ->
    default_theme = angular.copy($scope.theme)
    default_variables = angular.copy($scope.variables)

  editor.save = (opts = {publish: false}) ->
    #style = themeToLess()
    body = themeToHash()
    editor.busy = true
    theme_action = if opts.publish then "published" else "saved"
    themeEditorSvc.saveTheme(body,opts).then(
      ->
        editor.local_save()
        toastr.info("Theme #{theme_action}")
      -> toastr.error('Error while saving theme')
    ).finally(-> editor.busy = false)

  editor.export = () ->
    # Update the Text Area
    # editor.output = output = themeToLess()
    editor.output = output = angular.toJson(themeToHash(),true)

    # Create and download the less file
    anchor = angular.element('<a/>')
    anchor.css({display: 'none'}) # Make sure it's not visible
    angular.element(document.body).append(anchor) # Attach to document

    anchor.attr({
      href: 'data:attachment/csv;charset=utf-8,' + encodeURI(output),
      target: '_blank',
      download: 'live-previewer.json'
    })[0].click()

    anchor.remove() # Clean it up afterwards

    return true

  editor.import = ->
    editor.busy = true
    #loadThemeData(themeEditorSvc.parseLessVars(editor.output))
    loadThemeData(angular.fromJson(editor.output))
    editor.update().then ->
      editor.busy = false
      toastr.info('Theme has been imported')

  editor.updateLogo = () ->
    logo = angular.element($('#theme-main-logo'))[0].files[0]
    themeEditorSvc.saveLogo(logo).then(->
      toastr.info('Logo updated, please refresh the page')
    )

  #============================================
  # Private method
  #============================================
  # Convert the theme JS object to a less String
  themeToLess = ->
    output = "/****** Live Theme ****/\r\n/* Theme */ \r\n"
    _.forEach(theme, (value, key) ->
      variable = "#{key}: "
      output += _.padRight(variable, key_length) + value + ';\r\n'
    )

    output += "\r\n/* Variables */ \r\n"
    _.forEach(variables, (vars, section) ->
      output += "// #{section}\r\n"
      _.forEach(vars, (value, key) ->
        output += "#{key}: " + value + ';\r\n'
      )
      output += "\r\n"
    )

    return output

  themeToHash = ->
    return {
      branding: theme,
      variables: variables
    }

  # Build a JS object than can be passed to lessjs
  mergeLessVars = ->
    lessVars = angular.extend({}, theme)
    console.log(variables)
    _.forEach(variables, (vars, key) ->
      console.log(lessVars)
      angular.extend(lessVars, vars)
    )
    return lessVars

  # Load custom theme
  loadLastSavedTheme = ->
    themeEditorSvc.getTheme().then(
      (data) ->
        loadThemeData(data)
        editor.local_save()
      (error) ->
        $log.info('No custom theme found')
    )

  loadThemeData = (lessVars) ->
    data = flattenObject(lessVars)
    console.log(data)

    _.forEach(theme, (value, key) ->
      if data[key]
        theme[key] = data[key]
    )

    _.forEach(variables, (vars, section) ->
      _.forEach(vars, (value, key) ->
        if data[key]
          variables[section][key] = data[key]
      )
    )

  # Flatten an object with nested objects into
  # a single depth object
  flattenObject = (ob) ->
    flatObj = {}
    for i of ob
      continue unless ob.hasOwnProperty(i)
      if (typeof ob[i]) is "object"
        flatObject = flattenObject(ob[i])
        for x of flatObject
          continue unless flatObject.hasOwnProperty(x)
          flatObj[x] = flatObject[x]
      else
        flatObj[i] = ob[i]
    return flatObj

  # Init
  loadLastSavedTheme()
  toastr.info("Scroll down to start editing the dashboard style",null,{timeOut: 6000})

angular.module 'mnoEnterpriseAngular'
  .directive('themeEditor', ->
    return {
      restrict: 'EA'
      controller: ThemeEditorCtrl
      templateUrl: 'app/components/theme-editor/theme-editor.html',
    }
  )