Browse Source

inject container to routes functions as argument, buttons and inputs utils, button loaders

Rafał Pitoń 9 years ago
parent
commit
eab9c7b737

+ 47 - 0
misago/frontend/misago/components/button.js

@@ -0,0 +1,47 @@
+(function (Misago) {
+  'use strict';
+
+  var button = {
+    view: function(ctrl, kwargs) {
+      var options = {
+        disabled: kwargs.disabled || kwargs.loading || false,
+        config: kwargs.config || null,
+        loading: kwargs.loading || false,
+        type: kwargs.submit ? 'submit' : 'button',
+        onclick: kwargs.onclick || null
+      };
+
+      var element = 'button[type="' + options.type + '"].btn';
+      if (options.loading) {
+        element += '.btn-loading';
+      }
+
+      if (kwargs.id) {
+        element += '#' + kwargs.id;
+      }
+
+      element += (kwargs.class || '');
+
+      var label = kwargs.label;
+      if (options.loading) {
+        label = [
+          label,
+          m('.loader-compact', [
+            m('.bounce1'),
+            m('.bounce2'),
+            m('.bounce3')
+          ])
+        ];
+      }
+
+      return m(element, options, label);
+    },
+  };
+
+  Misago.addService('component:button', {
+    factory: function(_) {
+      _.component('button', button);
+    },
+    after: 'components'
+  });
+} (Misago.prototype));

+ 22 - 0
misago/frontend/misago/components/modals/header.js

@@ -0,0 +1,22 @@
+(function (Misago) {
+  'use strict';
+
+  var header = {
+    view: function(ctrl, title) {
+      return m('.modal-header', [
+        m('button.close[type="button"]',
+          {'data-dismiss': 'modal', 'aria-label': gettext('Close')},
+          m('span', {'aria-hidden': 'true'}, m.trust('×'))
+        ),
+        m('h4#misago-modal-label.modal-title', title)
+      ]);
+    }
+  };
+
+  Misago.addService('component:modal:header', {
+    factory: function(_) {
+      _.component('modal:header', header);
+    },
+    after: 'components'
+  });
+}(Misago.prototype));

+ 3 - 3
misago/frontend/misago/components/modals/register.js

@@ -26,10 +26,10 @@
     }
   };
 
-  Misago.addService('component:modal:register', {
+  Misago.addService('modal:register', {
     factory: function(_) {
-      _.component('modal:register', register);
+      _.modal('register', register);
     },
-    after: 'components'
+    after: 'modals'
   });
 }(Misago.prototype));

+ 47 - 32
misago/frontend/misago/components/modals/signin.js

@@ -7,70 +7,85 @@
 
   var signin = {
     controller: function() {
-      console.log('construct!');
       return {
-        busy: false,
+        busy: m.prop(false),
 
         username: m.prop(''),
         password: m.prop(''),
+      };
+    },
+    submit: function(_) {
+      if (this.busy()) {
+        return false;
+      }
 
-        validate: function() {
-          return false;
-        },
+      m.startComputation();
+      this.busy(true);
+      m.endComputation();
 
-        submit: function(e) {
-          console.log('SUBMITTING FORM!');
-          return false;
-        }
+      var credentials = {
+        username: $.trim(this.username()),
+        password: $.trim(this.password())
       };
+
+      var self = this;
+
+      _.api.endpoint('auth').post(credentials).then(
+      function(data) {
+        console.log(data);
+      },
+      function(error) {
+        console.log(error);
+      }).then(function() {
+        m.startComputation();
+        self.busy(false);
+        m.endComputation();
+      });
+
+      return false;
     },
-    view: function(ctrl) {
+    view: function(ctrl, _) {
       return m('.modal-dialog.modal-sm.modal-signin[role="document"]',
         {config: persistent},
-        m('.modal-content',
-          m('form', {onsubmit: ctrl.submit}, [
-            m('.modal-header',
-              m('button.close[type="button"]',
-                {'data-dismiss': 'modal', 'aria-label': gettext('Close')},
-                m('span', {'aria-hidden': 'true'}, m.trust('×'))
-              ),
-              m('h4#misago-modal-label.modal-title', 'Sign in')
-            ),
+        m('.modal-content', [
+          _.component('modal:header', gettext('Sign in')),
+          m('form', {onsubmit: this.submit.bind(ctrl, _)}, [
             m('.modal-body', [
               m('.form-group',
                 m('.control-input',
-                  m('input.form-control[type="text"]', {
-                    placeholder: gettext("Username or e-mail"),
-                    oninput: m.withAttr('value', ctrl.username),
-                    value: ctrl.username()
+                  Misago.input({
+                    disabled: ctrl.busy(),
+                    value: ctrl.password,
+                    placeholder: gettext("Username or e-mail")
                   })
                 )
               ),
               m('.form-group',
                 m('.control-input',
-                  m('input.form-control[type="password"]', {
-                    placeholder: gettext("Password"),
-                    oninput: m.withAttr('value', ctrl.password),
-                    value: ctrl.password()
+                  Misago.input({
+                    type: 'password',
+                    disabled: ctrl.busy(),
+                    value: ctrl.password,
+                    placeholder: gettext("Password")
                   })
                 )
               )
             ]),
             m('.modal-footer', [
               m('button.btn.btn-primary.btn-block[type="submit"]',
-                gettext('Sign in')
+                ctrl.busy() ? 'Working!!!' : gettext('Sign in')
               )
             ])
           ])
-        )
+        ])
       );
     }
   };
 
-  Misago.addService('component:modal:sign-in', {
+  Misago.addService('modal:sign-in', {
     factory: function(_) {
-      _.component('modal:sign-in', signin);
+      _.modal('sign-in', signin);
     },
-    after: 'components'
+    after: 'modals'
   });
 }(Misago.prototype));

+ 13 - 3
misago/frontend/misago/components/navbar/desktop/guest-menu.js

@@ -2,13 +2,23 @@
   'use strict';
 
   var menu = {
-    view: function(ctrl, _) {
+    controller: function(_) {
+      return {
+        showSignIn: function() {
+          _.modal('sign-in');
+        },
+        showRegister: function() {
+          _.modal('register');
+        }
+      };
+    },
+    view: function(ctrl) {
       return m('div.nav.guest-nav', [
         m('button.navbar-btn.btn.btn-default',
-          {onclick: function() {_.modal.show(Misago.SignInModal); }},
+          {onclick: ctrl.showSignIn},
           gettext("Sign in")),
         m('button.navbar-btn.btn.btn-primary',
-          {onclick: function() {_.modal.show(Misago.RegisterModal); }},
+          {onclick: ctrl.showRegister},
           gettext("Register"))
       ]);
     }

+ 20 - 10
misago/frontend/misago/routes/index.js

@@ -16,17 +16,27 @@
         }
       };
     },
-    view: function(ctrl) {
+    view: function(ctrl, _) {
+      var styles = [
+        'default', 'primary', 'success',
+        'info', 'warning', 'danger'
+      ];
+
       return m('.container', [
-        m('h1', [
-          'Count: ', m('strong', ctrl.count())
-        ]),
-        m('p', 'Clicky click button to increase count!.'),
-        m('p',
-          m('button.btn.btn-primary', {onclick: ctrl.increment},
-            'Clicky clicky!'
-          )
-        )
+        m('h1', 'Buttons'),
+        m('', styles.map(function(item) {
+          return m('', [
+            _.component('button', {
+              class: '.btn-' + item,
+              label: 'Lorem ipsum'
+            }),
+            _.component('button', {
+              class: '.btn-' + item,
+              label: 'Lorem ipsum',
+              loading: true
+            })
+          ]);
+        }))
       ]);
     }
   };

+ 2 - 6
misago/frontend/misago/routes/legal.js

@@ -5,9 +5,7 @@
     var dashedTypeName = typeName.replace(/_/g, '-');
 
     return {
-      controller: function() {
-        var _ = this.container;
-
+      controller: function(_) {
         if (Misago.get(_.settings, typeName + '_link')) {
           window.location = Misago.get(_.settings, typeName + '_link');
         } else {
@@ -43,9 +41,7 @@
           }
         }
       },
-      view: function() {
-        var _ = this.container;
-
+      view: function(ctrl, _) {
         return m('.page.legal-page.' + dashedTypeName + '-page', [
           _.component('header', {title: this.vm.page.title}),
           m('.container',

+ 1 - 1
misago/frontend/misago/services/ajax.js

@@ -30,7 +30,7 @@
           'X-CSRFToken': this.csrfToken
         },
 
-        data: data | {},
+        data: data || {},
         dataType: 'json',
 
         success: function(data) {

+ 49 - 0
misago/frontend/misago/services/modal-controller.js

@@ -0,0 +1,49 @@
+(function (Misago) {
+  'use strict';
+
+  var Modal = function() {
+    var self = this;
+
+    var element = document.getElementById('misago-modal');
+
+    // href clicks within modal should close it
+    var delegateName = 'click.misago-modal';
+    $(element).on(delegateName, 'a', function() {
+      self.hide();
+    });
+
+    this.destroy = function() {
+      $(element).off(delegateName);
+    };
+
+    // Open/close modal
+    var modal = $(element).modal({show: false});
+    this.open = false;
+
+    modal.on('hidden.bs.modal', function () {
+      if (self.open) {
+        m.mount(element, null);
+        this.open = false;
+      }
+    });
+
+    this.show = function(component) {
+      this.open = true;
+      m.mount(element, component);
+      modal.modal('show');
+    };
+
+    this.hide = function() {
+      modal.modal('hide');
+    };
+  };
+
+  Misago.addService('_modal', {
+    factory: function() {
+      return new Modal();
+    },
+    destroy: function(_) {
+      _.modal_.destroy();
+    }
+  }, {after: 'start-routing'});
+}(Misago.prototype));

+ 30 - 0
misago/frontend/misago/services/modals.js

@@ -0,0 +1,30 @@
+(function (Misago) {
+  'use strict';
+
+  var boilerplate = function(component) {
+    return component;
+  };
+
+  var modal = function(name, component) {
+    if (this._modals[name]) {
+      var argumentsArray = [this._modals[name]];
+      for (var i = 1; i < arguments.length; i += 1) {
+        argumentsArray.push(arguments[i]);
+      }
+      argumentsArray.push(this);
+      this._modal.show(m.component.apply(m, argumentsArray));
+    } else if (name) {
+      this._modals[name] = boilerplate(component);
+    } else {
+      this._modal.hide();
+    }
+  };
+
+  Misago.addService('modals', {
+    factory: function(_) {
+      _._modals = {};
+      _.modal = modal;
+    },
+    after: '_modal'
+  });
+}(Misago.prototype));

+ 18 - 69
misago/frontend/misago/services/routes.js

@@ -1,83 +1,32 @@
 (function (Misago) {
   'use strict';
 
-  var noop = function() {};
-
   var boilerplate = function(component) {
     /*
       Boilerplate for Misago top-level components
     */
 
-    // Component state
-    component.isActive = true;
-
-    // Wrap controller to store lifecycle methods
-    var _controller = component.controller || noop;
-    component.controller = function() {
-      component.isActive = true;
-
-      var controller = _controller.apply(component, arguments) || {};
-
-      // wrap onunload for lifestate
-      var _onunload = controller.onunload || noop;
-      controller.onunload = function() {
-        _onunload.apply(component, arguments);
-        component.isActive = false;
-      };
-
-      return controller;
+    // Component boilerplated (this may happen in tests)
+    if (component._hasRouteBoilerplate) {
+      return component;
+    }
+    component._hasRouteBoilerplate = true;
+
+    // Add lifecycle hooks
+    var loadingView = function () {
+      var _ = this.container;
+      return m('.page.page-loading',
+        _.component('loader')
+      );
     };
 
-    // Add state callbacks to View-Model
-    if (component.vm && component.vm.init) {
-      // wrap vm.init in promise handler
-      var _init = component.vm.init;
-      component.vm.init = function() {
-        var initArgs = arguments;
-        var promise = _init.apply(component.vm, initArgs);
-
-        if (promise) {
-          promise.then(function() {
-            if (component.isActive && component.vm.ondata) {
-              var finalArgs = [];
-              for (var i = 0; i < arguments.length; i++) {
-                finalArgs.push(arguments[i]);
-              }
-              for (var f = 0; f < initArgs.length; f++) {
-                finalArgs.push(initArgs[f]);
-              }
-
-              component.vm.ondata.apply(component.vm, finalArgs);
-            }
-          }, function(error) {
-            if (component.isActive) {
-              component.container.router.errorPage(error);
-            }
-          });
-        }
-      };
-
-      // setup default loading view
-      if (!component.loading) {
-        component.loading = function () {
-          var _ = this.container;
-          return m('.page.page-loading',
-            _.component('loader')
-          );
-        };
+    var errorHandler = function(error) {
+      if (this.isActive) {
+        this.container.router.errorPage(error);
       }
+    };
 
-      var _view = component.view;
-      component.view = function() {
-        if (component.vm.isReady) {
-          return _view.apply(component, arguments);
-        } else {
-          return component.loading.apply(component, arguments);
-        }
-      };
-    }
-
-    return component;
+    return Misago.stateHooks(component, loadingView, errorHandler);
   };
 
   Misago.addService('routes', function(_) {
@@ -85,7 +34,7 @@
     _.route = function(name, component) {
       if (name && component) {
         component.container = _;
-        this._routes[name] = boilerplate(component);
+        this._routes[name] = m.component(boilerplate(component), _);
       } else {
         return this._routes[name];
       }

+ 1 - 0
misago/frontend/misago/style/misago.less

@@ -53,6 +53,7 @@
 @import "misago/loaders.less";
 @import "misago/navbar.less";
 @import "misago/material-icons.less";
+@import "misago/buttons.less";
 
 // Pages
 @import "misago/error-pages.less";

+ 45 - 0
misago/frontend/misago/style/misago/buttons.less

@@ -0,0 +1,45 @@
+//
+// Buttons
+// --------------------------------------------------
+
+
+// Button loading style
+.btn.btn-loading {
+  &, &:link, &:active, &:visited, &:hover, &:focus {
+    // make text transparent, but still take place in button
+    color: transparent;
+
+    // position loader over the text
+    .loader-compact {
+      margin-top: @line-height-computed * -1;
+      height: 20px;
+
+      position: relative;
+      top: 1px;
+    }
+  }
+
+  &.btn-default .loader-compact > div {
+    background-color: @btn-default-color;
+  }
+
+  &.btn-primary .loader-compact > div {
+    background-color: @btn-primary-color;
+  }
+
+  &.btn-success .loader-compact > div {
+    background-color: @btn-success-color;
+  }
+
+  &.btn-info .loader-compact > div {
+    background-color: @btn-info-color;
+  }
+
+  &.btn-warning .loader-compact > div {
+    background-color: @btn-warning-color;
+  }
+
+  &.btn-danger .loader-compact > div {
+    background-color: @btn-danger-color;
+  }
+}

+ 44 - 0
misago/frontend/misago/style/misago/loaders.less

@@ -100,3 +100,47 @@
     }
   }
 }
+
+
+.loader-compact {
+  margin: 0px auto;
+  width: 70px;
+  text-align: center;
+
+  &>div {
+    width: @loader-compact-height;
+    height: @loader-compact-height;
+    background-color: #333;
+
+    border-radius: 100%;
+    display: inline-block;
+    -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
+    animation: sk-bouncedelay 1.4s infinite ease-in-out both;
+  }
+
+  .bounce1 {
+    -webkit-animation-delay: -0.32s;
+    animation-delay: -0.32s;
+  }
+
+  .bounce2 {
+    margin: 0px 4px;
+    -webkit-animation-delay: -0.16s;
+    animation-delay: -0.16s;
+  }
+
+  @-webkit-keyframes sk-bouncedelay {
+    0%, 80%, 100% { -webkit-transform: scale(0.33) }
+    40% { -webkit-transform: scale(1.0) }
+  }
+
+  @keyframes sk-bouncedelay {
+    0%, 80%, 100% {
+      -webkit-transform: scale(0.33);
+      transform: scale(0.33);
+    } 40% {
+      -webkit-transform: scale(1.0);
+      transform: scale(1.0);
+    }
+  }
+}

+ 2 - 0
misago/frontend/misago/style/misago/variables.less

@@ -14,6 +14,8 @@
 //** Loader color
 @loader-color: @gray-light;
 
+//** Compact loader dot height
+@loader-compact-height: 12px;
 
 //== Error pages
 //

+ 27 - 0
misago/frontend/misago/utils/inputs.js

@@ -0,0 +1,27 @@
+(function (Misago) {
+  'use strict';
+
+  Misago.input = function(kwargs) {
+    var options = {
+      disabled: kwargs.disabled || false,
+      placeholder: kwargs.placeholder || null,
+      config: kwargs.config || null
+    };
+
+    var element = 'input';
+
+    if (kwargs.id) {
+      element += '#' + kwargs.id;
+    }
+
+    element += '.form-control' + (kwargs.class || '');
+    element += '[type="' + (kwargs.type || 'text') + '"]';
+
+    if (kwargs.value) {
+      options.value = kwargs.value();
+      options.oninput = m.withAttr('value', kwargs.value);
+    }
+
+    return m(element, options);
+  };
+}(Misago.prototype));

+ 86 - 0
misago/frontend/misago/utils/lifecycle-hooks.js

@@ -0,0 +1,86 @@
+(function (Misago) {
+  'use strict';
+
+  var noop = function() {};
+
+  Misago.stateHooks = function(component, loadingState, errorState) {
+    /*
+      Boilerplate for Misago components with lifecycles
+    */
+
+    // Component boilerplated (this may happen in tests)
+    if (component._hasLifecycleHooks) {
+      return component;
+    }
+    component._hasLifecycleHooks = true;
+
+    // Component active state
+    component.isActive = true;
+
+    // Wrap controller to store lifecycle methods
+    var _controller = component.controller || noop;
+    component.controller = function() {
+      component.isActive = true;
+
+      var controller = _controller.apply(component, arguments) || {};
+
+      // wrap onunload for lifestate
+      var _onunload = controller.onunload || noop;
+      controller.onunload = function() {
+        _onunload.apply(component, arguments);
+        component.isActive = false;
+      };
+
+      return controller;
+    };
+
+    // Add state callbacks to View-Model
+    if (component.vm && component.vm.init) {
+      // setup default loading view
+      if (!component.loading) {
+        var loadingHandler = loadingState.bind(component);
+        component.loading = loadingHandler;
+      }
+
+      var _view = component.view;
+      component.view = function() {
+        if (component.vm.isReady) {
+          return _view.apply(component, arguments);
+        } else {
+          return component.loading.apply(component, arguments);
+        }
+      };
+
+      var errorHandler = errorState.bind(component);
+
+      // wrap vm.init in promise handler
+      var _init = component.vm.init;
+      component.vm.init = function() {
+        var initArgs = arguments;
+        var promise = _init.apply(component.vm, initArgs);
+
+        if (promise) {
+          promise.then(function() {
+            if (component.isActive && component.vm.ondata) {
+              var finalArgs = [];
+              for (var i = 0; i < arguments.length; i++) {
+                finalArgs.push(arguments[i]);
+              }
+              for (var f = 0; f < initArgs.length; f++) {
+                finalArgs.push(initArgs[f]);
+              }
+
+              component.vm.ondata.apply(component.vm, finalArgs);
+            }
+          }, function(error) {
+            if (component.isActive) {
+              errorHandler(error);
+            }
+          });
+        }
+      };
+    }
+
+    return component;
+  };
+}(Misago.prototype));