├── .gitignore ├── README.md ├── accounts-ui-semantic-ui-tests.js ├── accounts-ui-semantic-ui.js ├── login-buttons-dialogs.html ├── login-buttons-dialogs.js ├── login-buttons-dropdown.html ├── login-buttons-dropdown.js ├── login-buttons-session.js ├── login-buttons-single.html ├── login-buttons-single.js ├── login-buttons.html ├── login-buttons.js └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # accounts-ui-semantic-ui 2 | 3 | Meteor accounts-ui adapted and styled to work with Semantic UI. 4 | 5 | ## Installation 6 | 7 | With Meteor version 0.9.0 and above, run 8 | 9 | $ meteor add iandouglas:accounts-ui-semantic-ui 10 | 11 | You will need Semantic UI installed and its JS loaded before this package runs. I haven't been happy with the Semantic UI packages already out there, so I'm doing this just by throwing all the CSS and JS files in my `/client/lib` folder. 12 | 13 | This replaces/replicates the official `accounts-ui` package, so make sure to remove it if it's in your project. 14 | 15 | ## How to Use 16 | 17 | Just add `{{> loginButtons}}` to your template, as usual! 18 | 19 | Since I based all the code off of the official Meteor package, then the usual `Accounts.ui` configurations will work. I added one additional configuration option, however, which is `dropdownClasses`. That is, you can (optionally) add the following to your config, along with any other options you might want: 20 | 21 | Accounts.ui.config({ 22 | dropdownClasses: 'simple' 23 | }); 24 | 25 | The classes you specify will be added to the main `.ui.dropdown.item` element - I use this mostly to add a `simple` class to my dropdowns, but you might use it for any other classes. Note that I tried to make it somewhat intelligent, so that if the dropdown has the `simple` class, then it will not be initialized with `$('.dropdown').dropdown()`. 26 | 27 | Or you might add the following: 28 | 29 | Accounts.ui.config({ 30 | dropdownTransition: 'drop' 31 | }); 32 | 33 | If you specify a dropdownTransition, then `.dropdown()` will be called with the given transition. You can see a list of possible Semantic UI transitions [here](http://semantic-ui.com/modules/transition.html). Thanks to joryphillips for the inspiration for this option! 34 | 35 | These configuration options are optional! You can include zero, one, or both of them (and change the values as you see fit). Note that a "simple" dropdown cannot also have transitions applied to it. 36 | 37 | ## Custom Signup Fields 38 | 39 | One of my favorite features from [ian:accounts-ui-bootstrap-3](https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3) is the ease with which you can add extra signup fields. I have implemented (nearly verbatim) his code for doing so, and so to add extra signup fields, you can pretty much just reference [his documentation](https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3#custom-signup-options). 40 | 41 | There is one small difference, however. Since Semantic UI supports a couple different [checkbox types](http://semantic-ui.com/modules/checkbox.html), I have added a couple fields to support this. Also, I feel like he didn't completely document his validation method, which is pretty slick! All that being said, it's probably worthwhile to show his example with the extra tweaks: 42 | 43 | Accounts.ui.config({ 44 | extraSignupFields: [ 45 | { 46 | fieldName: 'first-name', 47 | fieldLabel: 'First name', 48 | inputType: 'text', 49 | visible: true, 50 | saveToProfile: true, 51 | validate: function(value, errorFunction) { 52 | if (value.trim() == '') { 53 | errorFunction('First name cannot be blank'); 54 | return false; 55 | } else { 56 | return true; 57 | } 58 | } 59 | }, 60 | { 61 | fieldName: 'last-name', 62 | fieldLabel: 'Last name', 63 | inputType: 'text', 64 | visible: true, 65 | saveToProfile: true 66 | }, 67 | { 68 | fieldName: 'checkbox', 69 | fieldLabel: 'Default checkbox with no JS', 70 | inputType: 'checkbox', 71 | visible: true, 72 | saveToProfile: false 73 | }, 74 | { 75 | fieldName: 'checkbox-js', 76 | fieldLabel: 'Default checkbox with JS', 77 | inputType: 'checkbox', 78 | visible: true, 79 | saveToProfile: false, 80 | useJS: true 81 | }, 82 | { 83 | fieldName: 'checkbox-slider-js', 84 | fieldLabel: 'Slider checkbox with JS', 85 | inputType: 'checkbox', 86 | visible: true, 87 | saveToProfile: false, 88 | fieldClasses: 'slider', 89 | useJS: true 90 | }, 91 | { 92 | fieldName: 'checkbox-toggle-js', 93 | fieldLabel: 'Toggle checkbox with JS', 94 | inputType: 'checkbox', 95 | visible: true, 96 | saveToProfile: false, 97 | fieldClasses: 'toggle', 98 | useJS: true 99 | }, 100 | ] 101 | }); 102 | 103 | ## Extra Content Within Logged In Dropdown 104 | 105 | Once again drawing inspiration from the bootstrap 3 accounts-ui plugin, you can now define a template, `_loginButtonsAdditionalLoggedInDropdownActions`, which will be rendered within the "logged in dropdown." Not much else to say about this - but boy oh boy, when you need this feature, you really need it! 106 | 107 | Remember that the template will be rendered as an immediate child of a `.menu` container, so you will likely want to be defining `.item`'s and what-not. 108 | 109 | ## Overview 110 | 111 | I am a relative newcomer to the Semantic UI framework, and could not find an implementation of accounts-ui using Semantic UI classes/markup, so I decided to try and put together my own. Overall I am very happy with this initial release - but I'm sure there are improvements that can be made, and I welcome pull requests or comments. 112 | 113 | Mostly, I just really wanted to be able to use the excellent accounts-ui and have it play nicely with all my other Semantic UI elements. 114 | 115 | ## Semantic UI Usage 116 | 117 | This package is mainly based around the dropdown component. I have done my best to make everything pretty within that dropdown, but honestly have found some of the default behavior styling to be somewhat lackluster/rigid, and hence some markup is abused and I included just a couple minor (but shameful) inline styles, to clean things up. 118 | 119 | The components used are (in order of "importance"/frequency): 120 | 121 | * [Dropdown](http://semantic-ui.com/modules/dropdown.html) (CSS and JS) 122 | * [Modal](http://semantic-ui.com/modules/modal.html) (CSS and JS) 123 | * [Menu](http://semantic-ui.com/collections/menu.html) (CSS) 124 | * [Form](http://semantic-ui.com/collections/form.html) (CSS) 125 | * [Button](http://semantic-ui.com/elements/button.html) (CSS) 126 | * [Divider](http://semantic-ui.com/elements/divider.html) (CSS) 127 | * [Loader](http://semantic-ui.com/elements/loader.html) (CSS) 128 | * [Transition](http://semantic-ui.com/modules/transition.html) (JS) 129 | 130 | ## Changelog 131 | 132 | ### Version 1.2.2 (2015-06-12) 133 | * Pushed previous version to atmosphere and immediately found a bug. That's how it goes, right? I should push all my projects to atmosphere - I bet I'd find bugs a lot quicker! 134 | * Fixed bug wherein another dropdown in the addition logged in dropdown template breaks the parent dropdown 135 | 136 | ### Version 1.2.1 (2015-06-12) 137 | * Added (optional) template, `_loginButtonsAdditionalLoggedInDropdownActions` which can be defined and will then be rendered within the logged in dropdown. 138 | 139 | ### Version 1.2.0 (2015-06-08) 140 | * Implementing more amazing fixes from joryphillips 141 | * Added dropdown transition option 142 | 143 | ### Version 1.1.1 (2015-04-29) 144 | * Fixed minor markdown typo in readme. 145 | 146 | ### Version 1.1.0 (2015-04-29) 147 | * Added cancel button to "change password" view (Thanks, joryphillips!) 148 | * Prevented dropdown from closing when switching between views (such as login -> register) (Thanks, joryphillips!) 149 | 150 | ### Version 1.0.3 (2015-03-26) 151 | * Added `extraSignupFields` option 152 | 153 | ### Version 1.0.2 (2015-03-23) 154 | * Initial version released to atmosphere 155 | -------------------------------------------------------------------------------- /accounts-ui-semantic-ui-tests.js: -------------------------------------------------------------------------------- 1 | // // XXX Most of the testing of accounts-ui is done manually, across 2 | // // multiple browsers using examples/unfinished/accounts-ui-helper. We 3 | // // should *definitely* automate this, but Tinytest is generally not 4 | // // the right abstraction to use for this. 5 | 6 | 7 | // // XXX it'd be cool to also test that the right thing happens if options 8 | // // *are* validated, but Accounts.ui._options is global state which makes this hard 9 | // // (impossible?) 10 | // Tinytest.add('accounts-ui - config validates keys', function (test) { 11 | // test.throws(function () { 12 | // Accounts.ui.config({foo: "bar"}); 13 | // }); 14 | 15 | // test.throws(function () { 16 | // Accounts.ui.config({passwordSignupFields: "not a valid option"}); 17 | // }); 18 | 19 | // test.throws(function () { 20 | // Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); 21 | // }); 22 | 23 | // test.throws(function () { 24 | // Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}}); 25 | // }); 26 | // }); 27 | -------------------------------------------------------------------------------- /accounts-ui-semantic-ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Accounts UI 3 | * @namespace 4 | * @memberOf Accounts 5 | */ 6 | Accounts.ui = {}; 7 | 8 | Accounts.ui._options = { 9 | requestPermissions: {}, 10 | requestOfflineToken: {}, 11 | forceApprovalPrompt: {}, 12 | extraSignupFields: [] 13 | }; 14 | 15 | // XXX refactor duplicated code in this function 16 | 17 | /** 18 | * @summary Configure the behavior of [`{{> loginButtons}}`](#accountsui). 19 | * @locus Client 20 | * @param {Object} options 21 | * @param {Object} options.requestPermissions Which [permissions](#requestpermissions) to request from the user for each external service. 22 | * @param {Object} options.requestOfflineToken To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details. 23 | * @param {Object} options.forceApprovalPrompt If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google. 24 | * @param {String} options.passwordSignupFields Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default). 25 | */ 26 | Accounts.ui.config = function(options) { 27 | // validate options keys 28 | var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt', 'dropdownClasses', 'extraSignupFields']; 29 | _.each(_.keys(options), function (key) { 30 | if (!_.contains(VALID_KEYS, key)) 31 | throw new Error("Accounts.ui.config: Invalid key: " + key); 32 | }); 33 | 34 | // deal with `dropdownClasses` 35 | if (options.dropdownClasses) { 36 | Accounts.ui._options.dropdownClasses = options.dropdownClasses; 37 | } 38 | 39 | // deal with extra signup fields 40 | // swiped from ian:accounts-ui-bootstrap-3... you rock! 41 | options.extraSignupFields = options.extraSignupFields || []; 42 | if (typeof options.extraSignupFields !== 'object' || !options.extraSignupFields instanceof Array) { 43 | throw new Error("Accounts.ui.config: `extraSignupFields` must be an array."); 44 | } else { 45 | if (options.extraSignupFields) { 46 | _.each(options.extraSignupFields, function(field, index) { 47 | if (!field.fieldName || !field.fieldLabel){ 48 | throw new Error("Accounts.ui.config: `extraSignupFields` objects must have `fieldName` and `fieldLabel` attributes."); 49 | } 50 | if (typeof field.visible === 'undefined'){ 51 | field.visible = true; 52 | } 53 | Accounts.ui._options.extraSignupFields[index] = field; 54 | }); 55 | } 56 | } 57 | 58 | // deal with `passwordSignupFields` 59 | if (options.passwordSignupFields) { 60 | if (_.contains([ 61 | "USERNAME_AND_EMAIL", 62 | "USERNAME_AND_OPTIONAL_EMAIL", 63 | "USERNAME_ONLY", 64 | "EMAIL_ONLY" 65 | ], options.passwordSignupFields)) { 66 | if (Accounts.ui._options.passwordSignupFields) 67 | throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once"); 68 | else 69 | Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; 70 | } else { 71 | throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); 72 | } 73 | } 74 | 75 | // deal with `requestPermissions` 76 | if (options.requestPermissions) { 77 | _.each(options.requestPermissions, function (scope, service) { 78 | if (Accounts.ui._options.requestPermissions[service]) { 79 | throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service); 80 | } else if (!(scope instanceof Array)) { 81 | throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array"); 82 | } else { 83 | Accounts.ui._options.requestPermissions[service] = scope; 84 | } 85 | }); 86 | } 87 | 88 | // deal with `requestOfflineToken` 89 | if (options.requestOfflineToken) { 90 | _.each(options.requestOfflineToken, function (value, service) { 91 | if (service !== 'google') 92 | throw new Error("Accounts.ui.config: `requestOfflineToken` only supported for Google login at the moment."); 93 | 94 | if (Accounts.ui._options.requestOfflineToken[service]) { 95 | throw new Error("Accounts.ui.config: Can't set `requestOfflineToken` more than once for " + service); 96 | } else { 97 | Accounts.ui._options.requestOfflineToken[service] = value; 98 | } 99 | }); 100 | } 101 | 102 | // deal with `forceApprovalPrompt` 103 | if (options.forceApprovalPrompt) { 104 | _.each(options.forceApprovalPrompt, function (value, service) { 105 | if (service !== 'google') 106 | throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment."); 107 | 108 | if (Accounts.ui._options.forceApprovalPrompt[service]) { 109 | throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service); 110 | } else { 111 | Accounts.ui._options.forceApprovalPrompt[service] = value; 112 | } 113 | }); 114 | } 115 | }; 116 | 117 | passwordSignupFields = function () { 118 | return Accounts.ui._options.passwordSignupFields || "EMAIL_ONLY"; 119 | }; 120 | 121 | -------------------------------------------------------------------------------- /login-buttons-dialogs.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 45 | 46 | 64 | 65 | 95 | 96 | 114 | 115 | 167 | 168 | 183 | 184 | 201 | -------------------------------------------------------------------------------- /login-buttons-dialogs.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | // since we don't want to pass around the callback that we get from our event 5 | // handlers, we just make it a variable for the whole file 6 | var doneCallback; 7 | 8 | Accounts.onResetPasswordLink(function (token, done) { 9 | loginButtonsSession.set("resetPasswordToken", token); 10 | doneCallback = done; 11 | }); 12 | 13 | Accounts.onEnrollmentLink(function (token, done) { 14 | loginButtonsSession.set("enrollAccountToken", token); 15 | doneCallback = done; 16 | }); 17 | 18 | Accounts.onEmailVerificationLink(function (token, done) { 19 | Accounts.verifyEmail(token, function (error) { 20 | if (! error) { 21 | loginButtonsSession.set('justVerifiedEmail', true); 22 | } 23 | 24 | done(); 25 | // XXX show something if there was an error. 26 | }); 27 | }); 28 | 29 | 30 | // 31 | // resetPasswordDialog template 32 | // 33 | 34 | Template._resetPasswordDialog.events({ 35 | 'click #login-buttons-reset-password-button': function () { 36 | resetPassword(); 37 | }, 38 | 'keypress #reset-password-new-password': function (event) { 39 | if (event.keyCode === 13) 40 | resetPassword(); 41 | }, 42 | 'click #login-buttons-cancel-reset-password': function (e, t) { 43 | t.$('.modal').modal('hide'); 44 | loginButtonsSession.set('resetPasswordToken', null); 45 | if (doneCallback) 46 | doneCallback(); 47 | } 48 | }); 49 | 50 | var resetPassword = function () { 51 | loginButtonsSession.resetMessages(); 52 | var newPassword = document.getElementById('reset-password-new-password').value; 53 | if (! validatePassword(newPassword)) 54 | return; 55 | 56 | Accounts.resetPassword( 57 | loginButtonsSession.get('resetPasswordToken'), newPassword, function (error) { 58 | if (error) { 59 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 60 | } else { 61 | $('.modal').modal('hide') 62 | loginButtonsSession.set('resetPasswordToken', null); 63 | loginButtonsSession.set('justResetPassword', true); 64 | if (doneCallback) 65 | doneCallback(); 66 | } 67 | }); 68 | }; 69 | 70 | Template._resetPasswordDialog.helpers({ 71 | inResetPasswordFlow: function () { 72 | return loginButtonsSession.get('resetPasswordToken'); 73 | } 74 | }); 75 | 76 | // 77 | // justResetPasswordDialog template 78 | // 79 | 80 | Template._justResetPasswordDialog.events({ 81 | 'click #just-verified-dismiss-button': function () { 82 | loginButtonsSession.set('justResetPassword', false); 83 | } 84 | }); 85 | 86 | Template._justResetPasswordDialog.helpers({ 87 | visible: function () { 88 | return loginButtonsSession.get('justResetPassword'); 89 | }, 90 | displayName: displayName 91 | }); 92 | 93 | 94 | 95 | // 96 | // enrollAccountDialog template 97 | // 98 | 99 | Template._enrollAccountDialog.events({ 100 | 'click #login-buttons-enroll-account-button': function () { 101 | enrollAccount(); 102 | }, 103 | 'keypress #enroll-account-password': function (event) { 104 | if (event.keyCode === 13) 105 | enrollAccount(); 106 | }, 107 | 'click #login-buttons-cancel-enroll-account': function (e, t) { 108 | t.$('.modal').modal('hide'); 109 | loginButtonsSession.set('enrollAccountToken', null); 110 | if (doneCallback) 111 | doneCallback(); 112 | } 113 | }); 114 | 115 | var enrollAccount = function () { 116 | loginButtonsSession.resetMessages(); 117 | var password = document.getElementById('enroll-account-password').value; 118 | if (!validatePassword(password)) 119 | return; 120 | 121 | Accounts.resetPassword( 122 | loginButtonsSession.get('enrollAccountToken'), password, 123 | function (error) { 124 | if (error) { 125 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 126 | } else { 127 | loginButtonsSession.set('enrollAccountToken', null); 128 | if (doneCallback) 129 | doneCallback(); 130 | } 131 | }); 132 | }; 133 | 134 | Template._enrollAccountDialog.helpers({ 135 | inEnrollAccountFlow: function () { 136 | return loginButtonsSession.get('enrollAccountToken'); 137 | } 138 | }); 139 | 140 | 141 | // 142 | // justVerifiedEmailDialog template 143 | // 144 | 145 | Template._justVerifiedEmailDialog.events({ 146 | 'click #just-verified-dismiss-button': function (e, t) { 147 | t.$('.modal').modal('hide'); 148 | loginButtonsSession.set('justVerifiedEmail', false); 149 | } 150 | }); 151 | 152 | Template._justVerifiedEmailDialog.helpers({ 153 | visible: function () { 154 | return loginButtonsSession.get('justVerifiedEmail'); 155 | }, 156 | displayName: displayName 157 | }); 158 | 159 | 160 | // 161 | // loginButtonsMessagesDialog template 162 | // 163 | 164 | Template._loginButtonsMessagesDialog.events({ 165 | 'click #messages-dialog-dismiss-button': function (e, t) { 166 | loginButtonsSession.resetMessages(); 167 | } 168 | }); 169 | 170 | Template._loginButtonsMessagesDialog.helpers({ 171 | visible: function () { 172 | var hasMessage = loginButtonsSession.get('infoMessage') || loginButtonsSession.get('errorMessage'); 173 | return ! dropdown() && hasMessage; 174 | } 175 | }); 176 | 177 | 178 | // 179 | // configureLoginServiceDialog template 180 | // 181 | 182 | Template._configureLoginServiceDialog.events({ 183 | 'click .configure-login-service-dismiss-button': function (e, t) { 184 | t.$('.modal').modal('hide'); 185 | loginButtonsSession.set('configureLoginServiceDialogVisible', false); 186 | }, 187 | 'click #configure-login-service-dialog-save-configuration': function (e, t) { 188 | if (loginButtonsSession.get('configureLoginServiceDialogVisible') && ! loginButtonsSession.get('configureLoginServiceDialogSaveDisabled')) { 189 | // Prepare the configuration document for this login service 190 | var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); 191 | var configuration = { 192 | service: serviceName 193 | }; 194 | 195 | // Fetch the value of each input field 196 | _.each(configurationFields(), function(field) { 197 | configuration[field.property] = document.getElementById('configure-login-service-dialog-' + field.property).value.replace(/^\s*|\s*$/g, ""); // trim() doesnt work on IE8; 198 | }); 199 | 200 | configuration.loginStyle = $('#configure-login-service-dialog input[name="loginStyle"]:checked').val(); 201 | 202 | // Configure this login service 203 | Accounts.connection.call("configureLoginService", configuration, function (error, result) { 204 | if (error) { 205 | Meteor._debug("Error configuring login service " + serviceName, error); 206 | } 207 | else { 208 | t.$('.modal').modal('hide'); 209 | loginButtonsSession.set('configureLoginServiceDialogVisible', false); 210 | } 211 | }); 212 | } 213 | }, 214 | // IE8 doesn't support the 'input' event, so we'll run this on the keyup as 215 | // well. (Keeping the 'input' event means that this also fires when you use 216 | // the mouse to change the contents of the field, eg 'Cut' menu item.) 217 | 'input, keyup input': function (event) { 218 | // if the event fired on one of the configuration input fields, 219 | // check whether we should enable the 'save configuration' button 220 | if (event.target.id.indexOf('configure-login-service-dialog') === 0) 221 | updateSaveDisabled(); 222 | } 223 | }); 224 | 225 | // check whether the 'save configuration' button should be enabled. 226 | // this is a really strange way to implement this and a Forms 227 | // Abstraction would make all of this reactive, and simpler. 228 | var updateSaveDisabled = function () { 229 | var anyFieldEmpty = _.any(configurationFields(), function(field) { 230 | return document.getElementById('configure-login-service-dialog-' + field.property).value === ''; 231 | }); 232 | 233 | loginButtonsSession.set('configureLoginServiceDialogSaveDisabled', anyFieldEmpty); 234 | }; 235 | 236 | // Returns the appropriate template for this login service. This 237 | // template should be defined in the service's package 238 | var configureLoginServiceDialogTemplateForService = function () { 239 | var serviceName = loginButtonsSession.get('configureLoginServiceDialogServiceName'); 240 | // XXX Service providers should be able to specify their configuration 241 | // template name. 242 | return Template['configureLoginServiceDialogFor' + (serviceName === 'meteor-developer' ? 'MeteorDeveloper' : capitalize(serviceName))]; 243 | }; 244 | 245 | var configurationFields = function () { 246 | var template = configureLoginServiceDialogTemplateForService(); 247 | return template.fields(); 248 | }; 249 | 250 | Template._configureLoginServiceDialog.helpers({ 251 | configurationFields: function () { 252 | return configurationFields(); 253 | }, 254 | visible: function () { 255 | return loginButtonsSession.get('configureLoginServiceDialogVisible'); 256 | }, 257 | configurationSteps: function () { 258 | // renders the appropriate template 259 | return configureLoginServiceDialogTemplateForService(); 260 | }, 261 | saveDisabled: function () { 262 | return loginButtonsSession.get('configureLoginServiceDialogSaveDisabled'); 263 | } 264 | }); 265 | 266 | // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js 267 | var capitalize = function(str){ 268 | str = str == null ? '' : String(str); 269 | return str.charAt(0).toUpperCase() + str.slice(1); 270 | }; 271 | 272 | Template._configureLoginOnDesktopDialog.helpers({ 273 | visible: function () { 274 | return loginButtonsSession.get('configureOnDesktopVisible'); 275 | } 276 | }); 277 | 278 | Template._configureLoginOnDesktopDialog.events({ 279 | 'click #configure-on-desktop-dismiss-button': function (e, t) { 280 | t.$('.modal').modal('hide'); 281 | loginButtonsSession.set('configureOnDesktopVisible', false); 282 | } 283 | }); 284 | -------------------------------------------------------------------------------- /login-buttons-dropdown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 41 | 42 | 43 | 44 | 45 | 57 | 58 | 76 | 77 | 81 | 82 | 131 | 132 | 148 | 149 | 155 | 156 | 157 | 175 | 176 | 204 | 205 | 226 | -------------------------------------------------------------------------------- /login-buttons-dropdown.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | // 5 | // Activate semantic ui dropdowns 6 | // 7 | Template._loginButtonsLoggedInDropdown.rendered = function() { 8 | // activate the dropdown if it is not a "simple" dropdown 9 | var dropdown = this.$('#login-dropdown'); 10 | maybeActivateSemanticDropdown(dropdown); 11 | 12 | } 13 | Template._loginButtonsLoggedOutDropdown.rendered = function() { 14 | // activate the dropdown if it is not a "simple" dropdown 15 | var dropdown = this.$('#login-dropdown'); 16 | maybeActivateSemanticDropdown(dropdown); 17 | } 18 | var maybeActivateSemanticDropdown = function(dropdownElement) { 19 | if (dropdownElement.length > 0) { 20 | // users can specify extra classes in the {{> loginButtons}} template helper, and hence the dropdown might be "simple" and not need any JS attached at all. 21 | if (! dropdownElement.hasClass('simple')) { 22 | dropdownOptions = { 23 | action: 'nothing', // when user clicks on button/item in dropdown, do not do anything (by default, it will close the dropdown) 24 | onChange: function() {} 25 | } 26 | if (Accounts.ui._options.dropdownTransition) { 27 | dropdownOptions.transition = Accounts.ui._options.dropdownTransition; 28 | } 29 | dropdownElement.dropdown(dropdownOptions); 30 | } 31 | } 32 | } 33 | 34 | // events shared between loginButtonsLoggedOutDropdown and 35 | // loginButtonsLoggedInDropdown 36 | Template.loginButtons.events({ 37 | 'click #login-name-link, click #login-sign-in-link': function (e) { 38 | e.preventDefault(); // semantic wants to close the dropdown when you change dropdown "views" 39 | loginButtonsSession.set('dropdownVisible', true); 40 | Tracker.flush(); 41 | correctDropdownZIndexes(); 42 | }, 43 | 'click .login-close-text': function () { 44 | loginButtonsSession.closeDropdown(); 45 | } 46 | }); 47 | 48 | 49 | // 50 | // loginButtonsLoggedInDropdown template and related 51 | // 52 | 53 | Template._loginButtonsLoggedInDropdown.events({ 54 | 'click #login-buttons-open-change-password': function(e) { 55 | e.preventDefault(); // semantic wants to close the dropdown when you change dropdown "views" 56 | loginButtonsSession.resetMessages(); 57 | loginButtonsSession.set('inChangePasswordFlow', true); 58 | } 59 | }); 60 | 61 | Template._loginButtonsLoggedInDropdown.helpers({ 62 | displayName: displayName, 63 | 64 | inChangePasswordFlow: function () { 65 | return loginButtonsSession.get('inChangePasswordFlow'); 66 | }, 67 | 68 | inMessageOnlyFlow: function () { 69 | return loginButtonsSession.get('inMessageOnlyFlow'); 70 | }, 71 | 72 | dropdownVisible: function () { 73 | return loginButtonsSession.get('dropdownVisible'); 74 | }, 75 | 76 | dropdownClasses: function () { 77 | return Accounts.ui._options.dropdownClasses || ''; 78 | } 79 | }); 80 | 81 | Template._loginButtonsLoggedInDropdownActions.helpers({ 82 | allowChangingPassword: function () { 83 | // it would be more correct to check whether the user has a password set, 84 | // but in order to do that we'd have to send more data down to the client, 85 | // and it'd be preferable not to send down the entire service.password document. 86 | // 87 | // instead we use the heuristic: if the user has a username or email set. 88 | var user = Meteor.user(); 89 | return user.username || (user.emails && user.emails[0] && user.emails[0].address); 90 | }, 91 | 92 | hasAdditionalDropdownTemplate: function() { 93 | return Template._loginButtonsAdditionalLoggedInDropdownActions !== undefined; 94 | } 95 | }); 96 | 97 | 98 | // 99 | // loginButtonsLoggedOutDropdown template and related 100 | // 101 | 102 | Template._loginButtonsLoggedOutDropdown.events({ 103 | 'click #login-buttons-password': function () { 104 | loginOrSignup(); 105 | }, 106 | 107 | 'keypress #forgot-password-email': function (event) { 108 | event.stopPropagation(); 109 | if (event.keyCode === 13) 110 | forgotPassword(); 111 | }, 112 | 113 | 'click #login-buttons-forgot-password': function (event) { 114 | event.stopPropagation(); 115 | forgotPassword(); 116 | }, 117 | 118 | 'click #signup-link': function (event) { 119 | event.stopPropagation(); 120 | loginButtonsSession.resetMessages(); 121 | 122 | // store values of fields before swtiching to the signup form 123 | var username = trimmedElementValueById('login-username'); 124 | var email = trimmedElementValueById('login-email'); 125 | var usernameOrEmail = trimmedElementValueById('login-username-or-email'); 126 | // notably not trimmed. a password could (?) start or end with a space 127 | var password = elementValueById('login-password'); 128 | 129 | loginButtonsSession.set('inSignupFlow', true); 130 | loginButtonsSession.set('inForgotPasswordFlow', false); 131 | // force the ui to update so that we have the approprate fields to fill in 132 | Tracker.flush(); 133 | 134 | // update new fields with appropriate defaults 135 | if (username !== null) 136 | document.getElementById('login-username').value = username; 137 | else if (email !== null) 138 | document.getElementById('login-email').value = email; 139 | else if (usernameOrEmail !== null) 140 | if (usernameOrEmail.indexOf('@') === -1) 141 | document.getElementById('login-username').value = usernameOrEmail; 142 | else 143 | document.getElementById('login-email').value = usernameOrEmail; 144 | 145 | if (password !== null) 146 | document.getElementById('login-password').value = password; 147 | 148 | // Force redrawing the `login-dropdown-list` element because of 149 | // a bizarre Chrome bug in which part of the DIV is not redrawn 150 | // in case you had tried to unsuccessfully log in before 151 | // switching to the signup form. 152 | // 153 | // Found tip on how to force a redraw on 154 | // http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes/3485654#3485654 155 | var redraw = document.getElementById('login-dropdown-list'); 156 | redraw.style.display = 'none'; 157 | redraw.offsetHeight; // it seems that this line does nothing but is necessary for the redraw to work 158 | redraw.style.display = 'block'; 159 | }, 160 | 'click #forgot-password-link': function (event) { 161 | event.stopPropagation(); 162 | loginButtonsSession.resetMessages(); 163 | 164 | // store values of fields before swtiching to the signup form 165 | var email = trimmedElementValueById('login-email'); 166 | var usernameOrEmail = trimmedElementValueById('login-username-or-email'); 167 | 168 | loginButtonsSession.set('inSignupFlow', false); 169 | loginButtonsSession.set('inForgotPasswordFlow', true); 170 | // force the ui to update so that we have the approprate fields to fill in 171 | Tracker.flush(); 172 | 173 | // update new fields with appropriate defaults 174 | if (email !== null) 175 | document.getElementById('forgot-password-email').value = email; 176 | else if (usernameOrEmail !== null) 177 | if (usernameOrEmail.indexOf('@') !== -1) 178 | document.getElementById('forgot-password-email').value = usernameOrEmail; 179 | 180 | }, 181 | 'click #back-to-login-link': function (event) { 182 | event.stopPropagation(); 183 | loginButtonsSession.resetMessages(); 184 | 185 | var username = trimmedElementValueById('login-username'); 186 | var email = trimmedElementValueById('login-email') || trimmedElementValueById('forgot-password-email'); // Ughh. Standardize on names? 187 | // notably not trimmed. a password could (?) start or end with a space 188 | var password = elementValueById('login-password'); 189 | 190 | loginButtonsSession.set('inSignupFlow', false); 191 | loginButtonsSession.set('inForgotPasswordFlow', false); 192 | // force the ui to update so that we have the approprate fields to fill in 193 | Tracker.flush(); 194 | 195 | if (document.getElementById('login-username')) 196 | document.getElementById('login-username').value = username; 197 | if (document.getElementById('login-email')) 198 | document.getElementById('login-email').value = email; 199 | 200 | if (document.getElementById('login-username-or-email')) 201 | document.getElementById('login-username-or-email').value = email || username; 202 | 203 | if (password !== null) 204 | document.getElementById('login-password').value = password; 205 | }, 206 | 'keypress #login-username, keypress #login-email, keypress #login-username-or-email, keypress #login-password, keypress #login-password-again': function (event) { 207 | if (event.keyCode === 13) 208 | loginOrSignup(); 209 | } 210 | }); 211 | 212 | Template._loginButtonsLoggedOutDropdown.helpers({ 213 | // additional classes that can be helpful in styling the dropdown 214 | additionalClasses: function () { 215 | if (!hasPasswordService()) { 216 | return false; 217 | } else { 218 | if (loginButtonsSession.get('inSignupFlow')) { 219 | return 'login-form-create-account'; 220 | } else if (loginButtonsSession.get('inForgotPasswordFlow')) { 221 | return 'login-form-forgot-password'; 222 | } else { 223 | return 'login-form-sign-in'; 224 | } 225 | } 226 | }, 227 | 228 | dropdownVisible: function () { 229 | return loginButtonsSession.get('dropdownVisible'); 230 | }, 231 | 232 | hasPasswordService: hasPasswordService, 233 | 234 | dropdownClasses: function () { 235 | return Accounts.ui._options.dropdownClasses || ''; 236 | } 237 | }); 238 | 239 | // return all login services, with password last 240 | Template._loginButtonsLoggedOutAllServices.helpers({ 241 | services: getLoginServices, 242 | 243 | isPasswordService: function () { 244 | return this.name === 'password'; 245 | }, 246 | 247 | hasOtherServices: function () { 248 | return getLoginServices().length > 1; 249 | }, 250 | 251 | hasPasswordService: hasPasswordService 252 | }); 253 | 254 | Template._loginButtonsLoggedOutPasswordService.helpers({ 255 | fields: function () { 256 | var loginFields = [ 257 | { 258 | fieldName: 'username-or-email', 259 | fieldLabel: 'Username or Email', 260 | visible: function () { 261 | return _.contains( 262 | ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL"], 263 | passwordSignupFields() 264 | ); 265 | } 266 | }, 267 | { 268 | fieldName: 'username', 269 | fieldLabel: 'Username', 270 | visible: function () { 271 | return passwordSignupFields() === "USERNAME_ONLY"; 272 | } 273 | }, 274 | { 275 | fieldName: 'email', 276 | fieldLabel: 'Email', 277 | inputType: 'email', 278 | visible: function () { 279 | return passwordSignupFields() === "EMAIL_ONLY"; 280 | } 281 | }, 282 | { 283 | fieldName: 'password', 284 | fieldLabel: 'Password', 285 | inputType: 'password', 286 | visible: function () { 287 | return true; 288 | } 289 | } 290 | ]; 291 | 292 | var signupFields = [ 293 | { 294 | fieldName: 'username', 295 | fieldLabel: 'Username', 296 | visible: function () { 297 | return _.contains( 298 | ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], 299 | passwordSignupFields()); 300 | } 301 | }, 302 | { 303 | fieldName: 'email', 304 | fieldLabel: 'Email', 305 | inputType: 'email', 306 | visible: function () { 307 | return _.contains( 308 | ["USERNAME_AND_EMAIL", "EMAIL_ONLY"], 309 | passwordSignupFields()); 310 | } 311 | }, 312 | { 313 | fieldName: 'email', 314 | fieldLabel: 'Email (optional)', 315 | inputType: 'email', 316 | visible: function () { 317 | return passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL"; 318 | } 319 | }, 320 | { 321 | fieldName: 'password', 322 | fieldLabel: 'Password', 323 | inputType: 'password', 324 | visible: function () { 325 | return true; 326 | } 327 | }, 328 | { 329 | fieldName: 'password-again', 330 | fieldLabel: 'Password (again)', 331 | inputType: 'password', 332 | visible: function () { 333 | // No need to make users double-enter their password if 334 | // they'll necessarily have an email set, since they can use 335 | // the "forgot password" flow. 336 | return _.contains( 337 | ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], 338 | passwordSignupFields() 339 | ); 340 | } 341 | } 342 | ]; 343 | 344 | signupFields = signupFields.concat(Accounts.ui._options.extraSignupFields); 345 | 346 | return loginButtonsSession.get('inSignupFlow') ? signupFields : loginFields; 347 | }, 348 | 349 | inForgotPasswordFlow: function () { 350 | return loginButtonsSession.get('inForgotPasswordFlow'); 351 | }, 352 | 353 | inLoginFlow: function () { 354 | return !loginButtonsSession.get('inSignupFlow') && !loginButtonsSession.get('inForgotPasswordFlow'); 355 | }, 356 | 357 | inSignupFlow: function () { 358 | return loginButtonsSession.get('inSignupFlow'); 359 | }, 360 | 361 | showCreateAccountLink: function () { 362 | return !Accounts._options.forbidClientAccountCreation; 363 | }, 364 | 365 | showForgotPasswordLink: function () { 366 | return _.contains( 367 | ["USERNAME_AND_EMAIL", "USERNAME_AND_OPTIONAL_EMAIL", "EMAIL_ONLY"], 368 | passwordSignupFields() 369 | ); 370 | } 371 | }); 372 | 373 | Template._loginButtonsFormField.helpers({ 374 | inputType: function () { 375 | return this.inputType || "text"; 376 | }, 377 | equals: function(a, b) { 378 | return (a === b); 379 | } 380 | }); 381 | 382 | 383 | // 384 | // loginButtonsChangePassword template 385 | // 386 | 387 | Template._loginButtonsChangePassword.events({ 388 | 'keypress #login-old-password, keypress #login-password, keypress #login-password-again': function (event) { 389 | if (event.keyCode === 13) 390 | changePassword(); 391 | }, 392 | 'click #login-buttons-do-change-password': function () { 393 | changePassword(); 394 | }, 395 | 'click #login-buttons-cancel-change-password': function(event) { 396 | event.stopPropagation(); 397 | loginButtonsSession.resetMessages(); 398 | Accounts._loginButtonsSession.set('inChangePasswordFlow', false); 399 | Meteor.flush(); 400 | } 401 | }); 402 | 403 | Template._loginButtonsChangePassword.helpers({ 404 | fields: function () { 405 | return [ 406 | { 407 | fieldName: 'old-password', 408 | fieldLabel: 'Current Password', 409 | inputType: 'password', 410 | visible: function () { 411 | return true; 412 | } 413 | }, 414 | { 415 | fieldName: 'password', 416 | fieldLabel: 'New Password', 417 | inputType: 'password', 418 | visible: function () { 419 | return true; 420 | } 421 | }, 422 | { 423 | fieldName: 'password-again', 424 | fieldLabel: 'New Password (again)', 425 | inputType: 'password', 426 | visible: function () { 427 | // No need to make users double-enter their password if 428 | // they'll necessarily have an email set, since they can use 429 | // the "forgot password" flow. 430 | return _.contains( ["USERNAME_AND_OPTIONAL_EMAIL", "USERNAME_ONLY"], passwordSignupFields()); 431 | } 432 | } 433 | ]; 434 | } 435 | }); 436 | 437 | 438 | // 439 | // helpers 440 | // 441 | 442 | var elementValueById = function(id) { 443 | var element = document.getElementById(id); 444 | if (!element) 445 | return null; 446 | else 447 | return element.value; 448 | }; 449 | 450 | var trimmedElementValueById = function(id) { 451 | var element = document.getElementById(id); 452 | if (!element) 453 | return null; 454 | else 455 | return element.value.replace(/^\s*|\s*$/g, ""); // trim() doesn't work on IE8; 456 | }; 457 | 458 | var loginOrSignup = function () { 459 | if (loginButtonsSession.get('inSignupFlow')) 460 | signup(); 461 | else 462 | login(); 463 | }; 464 | 465 | var login = function () { 466 | loginButtonsSession.resetMessages(); 467 | 468 | var username = trimmedElementValueById('login-username'); 469 | var email = trimmedElementValueById('login-email'); 470 | var usernameOrEmail = trimmedElementValueById('login-username-or-email'); 471 | // notably not trimmed. a password could (?) start or end with a space 472 | var password = elementValueById('login-password'); 473 | 474 | var loginSelector; 475 | if (username !== null) { 476 | if (!validateUsername(username)) 477 | return; 478 | else 479 | loginSelector = {username: username}; 480 | } else if (email !== null) { 481 | if (!validateEmail(email)) 482 | return; 483 | else 484 | loginSelector = {email: email}; 485 | } else if (usernameOrEmail !== null) { 486 | // XXX not sure how we should validate this. but this seems good enough (for now), 487 | // since an email must have at least 3 characters anyways 488 | if (!validateUsername(usernameOrEmail)) 489 | return; 490 | else 491 | loginSelector = usernameOrEmail; 492 | } else { 493 | throw new Error("Unexpected -- no element to use as a login user selector"); 494 | } 495 | 496 | Meteor.loginWithPassword(loginSelector, password, function (error, result) { 497 | if (error) { 498 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 499 | } else { 500 | loginButtonsSession.closeDropdown(); 501 | } 502 | }); 503 | }; 504 | 505 | var signup = function () { 506 | loginButtonsSession.resetMessages(); 507 | 508 | var options = {}; // to be passed to Accounts.createUser 509 | 510 | var username = trimmedElementValueById('login-username'); 511 | if (username !== null) { 512 | if (!validateUsername(username)) 513 | return; 514 | else 515 | options.username = username; 516 | } 517 | 518 | var email = trimmedElementValueById('login-email'); 519 | if (email !== null) { 520 | if (!validateEmail(email)) 521 | return; 522 | else 523 | options.email = email; 524 | } 525 | 526 | // notably not trimmed. a password could (?) start or end with a space 527 | var password = elementValueById('login-password'); 528 | if (!validatePassword(password)) 529 | return; 530 | else 531 | options.password = password; 532 | 533 | if (!matchPasswordAgainIfPresent()) 534 | return; 535 | 536 | // 537 | // Parsing and storing extra signup fields. Code from ian:accounts-ui-bootstrap-3 538 | // 539 | 540 | // prepare the profile object if needed 541 | if (! (options.profile instanceof Object)) { 542 | options.profile = {}; 543 | } 544 | 545 | // define a proxy function to allow extraSignupFields to set error messages 546 | var errorFunction = function(errorMessage) { 547 | Accounts._loginButtonsSession.errorMessage(errorMessage); 548 | } 549 | 550 | var invalidExtraSignupFields = false; 551 | 552 | // parse and populate fields 553 | _.each(Accounts.ui._options.extraSignupFields, function(field, index) { 554 | var value = elementValueById('login-' + field.fieldName); 555 | if (typeof field.validate == 'function') { 556 | if (field.validate(value, errorFunction)) { 557 | if (typeof field.saveToProfile !== 'undefined' && ! field.saveToProfile) { 558 | options[field.fieldName] = value; 559 | } else { 560 | options.profile[field.fieldName] = value; 561 | } 562 | } else { 563 | invalidExtraSignupFields = true; 564 | } 565 | } else { 566 | options.profile[field.fieldName] = value; 567 | } 568 | }); 569 | 570 | if (invalidExtraSignupFields) { 571 | return; 572 | } 573 | 574 | Accounts.createUser(options, function (error) { 575 | if (error) { 576 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 577 | } else { 578 | loginButtonsSession.closeDropdown(); 579 | } 580 | }); 581 | }; 582 | 583 | var forgotPassword = function () { 584 | loginButtonsSession.resetMessages(); 585 | 586 | var email = trimmedElementValueById("forgot-password-email"); 587 | if (email.indexOf('@') !== -1) { 588 | Accounts.forgotPassword({email: email}, function (error) { 589 | if (error) 590 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 591 | else 592 | loginButtonsSession.infoMessage("Email sent"); 593 | }); 594 | } else { 595 | loginButtonsSession.errorMessage("Invalid email"); 596 | } 597 | }; 598 | 599 | var changePassword = function () { 600 | loginButtonsSession.resetMessages(); 601 | 602 | // notably not trimmed. a password could (?) start or end with a space 603 | var oldPassword = elementValueById('login-old-password'); 604 | 605 | // notably not trimmed. a password could (?) start or end with a space 606 | var password = elementValueById('login-password'); 607 | if (!validatePassword(password)) 608 | return; 609 | 610 | if (!matchPasswordAgainIfPresent()) 611 | return; 612 | 613 | Accounts.changePassword(oldPassword, password, function (error) { 614 | if (error) { 615 | loginButtonsSession.errorMessage(error.reason || "Unknown error"); 616 | } else { 617 | loginButtonsSession.set('inChangePasswordFlow', false); 618 | // inMessageOnlyFlow is what is messing things up -- removing it doesn't result in the most elegant solution, but it works for now 619 | // loginButtonsSession.set('inMessageOnlyFlow', true); 620 | loginButtonsSession.infoMessage("Password changed"); 621 | 622 | // wait 3 seconds, then expire the msg **adding from bootstrap version 623 | Meteor.setTimeout(function() { 624 | loginButtonsSession.resetMessages(); 625 | }, 3000); 626 | } 627 | }); 628 | }; 629 | 630 | var matchPasswordAgainIfPresent = function () { 631 | // notably not trimmed. a password could (?) start or end with a space 632 | var passwordAgain = elementValueById('login-password-again'); 633 | if (passwordAgain !== null) { 634 | // notably not trimmed. a password could (?) start or end with a space 635 | var password = elementValueById('login-password'); 636 | if (password !== passwordAgain) { 637 | loginButtonsSession.errorMessage("Passwords don't match"); 638 | return false; 639 | } 640 | } 641 | return true; 642 | }; 643 | 644 | var correctDropdownZIndexes = function () { 645 | // IE <= 7 has a z-index bug that means we can't just give the 646 | // dropdown a z-index and expect it to stack above the rest of 647 | // the page even if nothing else has a z-index. The nature of 648 | // the bug is that all positioned elements are considered to 649 | // have z-index:0 (not auto) and therefore start new stacking 650 | // contexts, with ties broken by page order. 651 | // 652 | // The fix, then is to give z-index:1 to all ancestors 653 | // of the dropdown having z-index:0. 654 | for(var n = document.getElementById('login-dropdown-list').parentNode; n.nodeName !== 'BODY'; n = n.parentNode) 655 | if (n.style.zIndex === 0) 656 | n.style.zIndex = 1; 657 | }; 658 | -------------------------------------------------------------------------------- /login-buttons-session.js: -------------------------------------------------------------------------------- 1 | var VALID_KEYS = [ 2 | 'dropdownVisible', 3 | 4 | // XXX consider replacing these with one key that has an enum for values. 5 | 'inSignupFlow', 6 | 'inForgotPasswordFlow', 7 | 'inChangePasswordFlow', 8 | 'inMessageOnlyFlow', 9 | 10 | 'errorMessage', 11 | 'infoMessage', 12 | 13 | // dialogs with messages (info and error) 14 | 'resetPasswordToken', 15 | 'enrollAccountToken', 16 | 'justVerifiedEmail', 17 | 'justResetPassword', 18 | 19 | 'configureLoginServiceDialogVisible', 20 | 'configureLoginServiceDialogServiceName', 21 | 'configureLoginServiceDialogSaveDisabled', 22 | 'configureOnDesktopVisible' 23 | ]; 24 | 25 | var validateKey = function (key) { 26 | if (!_.contains(VALID_KEYS, key)) 27 | throw new Error("Invalid key in loginButtonsSession: " + key); 28 | }; 29 | 30 | var KEY_PREFIX = "Meteor.loginButtons."; 31 | 32 | // XXX This should probably be package scope rather than exported 33 | // (there was even a comment to that effect here from before we had 34 | // namespacing) but accounts-ui-viewer uses it, so leave it as is for 35 | // now 36 | Accounts._loginButtonsSession = { 37 | set: function(key, value) { 38 | validateKey(key); 39 | if (_.contains(['errorMessage', 'infoMessage'], key)) 40 | throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage()."); 41 | 42 | this._set(key, value); 43 | }, 44 | 45 | _set: function(key, value) { 46 | Session.set(KEY_PREFIX + key, value); 47 | }, 48 | 49 | get: function(key) { 50 | validateKey(key); 51 | return Session.get(KEY_PREFIX + key); 52 | }, 53 | 54 | closeDropdown: function () { 55 | this.set('inSignupFlow', false); 56 | this.set('inForgotPasswordFlow', false); 57 | this.set('inChangePasswordFlow', false); 58 | this.set('inMessageOnlyFlow', false); 59 | this.set('dropdownVisible', false); 60 | this.resetMessages(); 61 | }, 62 | 63 | infoMessage: function(message) { 64 | this._set("errorMessage", null); 65 | this._set("infoMessage", message); 66 | this.ensureMessageVisible(); 67 | }, 68 | 69 | errorMessage: function(message) { 70 | this._set("errorMessage", message); 71 | this._set("infoMessage", null); 72 | this.ensureMessageVisible(); 73 | }, 74 | 75 | // is there a visible dialog that shows messages (info and error) 76 | isMessageDialogVisible: function () { 77 | return this.get('resetPasswordToken') || 78 | this.get('enrollAccountToken') || 79 | this.get('justVerifiedEmail'); 80 | }, 81 | 82 | // ensure that somethings displaying a message (info or error) is 83 | // visible. if a dialog with messages is open, do nothing; 84 | // otherwise open the dropdown. 85 | // 86 | // notably this doesn't matter when only displaying a single login 87 | // button since then we have an explicit message dialog 88 | // (_loginButtonsMessageDialog), and dropdownVisible is ignored in 89 | // this case. 90 | ensureMessageVisible: function () { 91 | if (!this.isMessageDialogVisible()) 92 | this.set("dropdownVisible", true); 93 | }, 94 | 95 | resetMessages: function () { 96 | this._set("errorMessage", null); 97 | this._set("infoMessage", null); 98 | }, 99 | 100 | configureService: function (name) { 101 | if (Meteor.isCordova) { 102 | this.set('configureOnDesktopVisible', true); 103 | } else { 104 | this.set('configureLoginServiceDialogVisible', true); 105 | this.set('configureLoginServiceDialogServiceName', name); 106 | this.set('configureLoginServiceDialogSaveDisabled', true); 107 | } 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /login-buttons-single.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | 20 | 30 | 31 | -------------------------------------------------------------------------------- /login-buttons-single.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | 5 | var loginResultCallback = function (serviceName, err) { 6 | if (!err) { 7 | loginButtonsSession.closeDropdown(); 8 | } else if (err instanceof Accounts.LoginCancelledError) { 9 | // do nothing 10 | } else if (err instanceof ServiceConfiguration.ConfigError) { 11 | loginButtonsSession.configureService(serviceName); 12 | } else { 13 | loginButtonsSession.errorMessage(err.reason || "Unknown error"); 14 | } 15 | }; 16 | 17 | 18 | // In the login redirect flow, we'll have the result of the login 19 | // attempt at page load time when we're redirected back to the 20 | // application. Register a callback to update the UI (i.e. to close 21 | // the dialog on a successful login or display the error on a failed 22 | // login). 23 | // 24 | Accounts.onPageLoadLogin(function (attemptInfo) { 25 | // Ignore if we have a left over login attempt for a service that is no longer registered. 26 | if (_.contains(_.pluck(getLoginServices(), "name"), attemptInfo.type)) 27 | loginResultCallback(attemptInfo.type, attemptInfo.error); 28 | }); 29 | 30 | 31 | Template._loginButtonsLoggedOutSingleLoginButton.events({ 32 | 'click .login-button': function () { 33 | var serviceName = this.name; 34 | loginButtonsSession.resetMessages(); 35 | 36 | // XXX Service providers should be able to specify their 37 | // `Meteor.loginWithX` method name. 38 | var loginWithService = Meteor["loginWith" + 39 | (serviceName === 'meteor-developer' ? 40 | 'MeteorDeveloperAccount' : 41 | capitalize(serviceName))]; 42 | 43 | var options = {}; // use default scope unless specified 44 | if (Accounts.ui._options.requestPermissions[serviceName]) 45 | options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName]; 46 | if (Accounts.ui._options.requestOfflineToken[serviceName]) 47 | options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]; 48 | if (Accounts.ui._options.forceApprovalPrompt[serviceName]) 49 | options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName]; 50 | 51 | loginWithService(options, function (err) { 52 | loginResultCallback(serviceName, err); 53 | }); 54 | } 55 | }); 56 | 57 | Template._loginButtonsLoggedOutSingleLoginButton.helpers({ 58 | configured: function () { 59 | return !!ServiceConfiguration.configurations.findOne({service: this.name}); 60 | }, 61 | capitalizedName: function () { 62 | if (this.name === 'github') 63 | // XXX we should allow service packages to set their capitalized name 64 | return 'GitHub'; 65 | else if (this.name === 'meteor-developer') 66 | return 'Meteor'; 67 | else 68 | return capitalize(this.name); 69 | } 70 | }); 71 | 72 | // XXX from http://epeli.github.com/underscore.string/lib/underscore.string.js 73 | var capitalize = function(str){ 74 | str = str == null ? '' : String(str); 75 | return str.charAt(0).toUpperCase() + str.slice(1); 76 | }; 77 | -------------------------------------------------------------------------------- /login-buttons.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 26 | 45 | 46 | 47 | 52 | 53 | 69 | 70 | 73 | 74 | 87 | -------------------------------------------------------------------------------- /login-buttons.js: -------------------------------------------------------------------------------- 1 | // for convenience 2 | var loginButtonsSession = Accounts._loginButtonsSession; 3 | 4 | // shared between dropdown and single mode 5 | Template.loginButtons.events({ 6 | 'click #login-buttons-logout': function() { 7 | Meteor.logout(function () { 8 | loginButtonsSession.closeDropdown(); 9 | }); 10 | } 11 | }); 12 | 13 | Template.registerHelper('loginButtons', function () { 14 | throw new Error("Use {{> loginButtons}} instead of {{loginButtons}}"); 15 | }); 16 | 17 | // 18 | // helpers 19 | // 20 | 21 | displayName = function () { 22 | var user = Meteor.user(); 23 | if (!user) 24 | return ''; 25 | 26 | if (user.profile && user.profile.name) 27 | return user.profile.name; 28 | if (user.username) 29 | return user.username; 30 | if (user.emails && user.emails[0] && user.emails[0].address) 31 | return user.emails[0].address; 32 | 33 | return ''; 34 | }; 35 | 36 | // returns an array of the login services used by this app. each 37 | // element of the array is an object (eg {name: 'facebook'}), since 38 | // that makes it useful in combination with handlebars {{#each}}. 39 | // 40 | // don't cache the output of this function: if called during startup (before 41 | // oauth packages load) it might not include them all. 42 | // 43 | // NOTE: It is very important to have this return password last 44 | // because of the way we render the different providers in 45 | // login_buttons_dropdown.html 46 | getLoginServices = function () { 47 | var self = this; 48 | 49 | // First look for OAuth services. 50 | var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : []; 51 | 52 | // Be equally kind to all login services. This also preserves 53 | // backwards-compatibility. (But maybe order should be 54 | // configurable?) 55 | services.sort(); 56 | 57 | // Add password, if it's there; it must come last. 58 | if (hasPasswordService()) 59 | services.push('password'); 60 | 61 | return _.map(services, function(name) { 62 | return {name: name}; 63 | }); 64 | }; 65 | 66 | hasPasswordService = function () { 67 | return !!Package['accounts-password']; 68 | }; 69 | 70 | dropdown = function () { 71 | return hasPasswordService() || getLoginServices().length > 1; 72 | }; 73 | 74 | // XXX improve these. should this be in accounts-password instead? 75 | // 76 | // XXX these will become configurable, and will be validated on 77 | // the server as well. 78 | validateUsername = function (username) { 79 | if (username.length >= 3) { 80 | return true; 81 | } else { 82 | loginButtonsSession.errorMessage("Username must be at least 3 characters long"); 83 | return false; 84 | } 85 | }; 86 | validateEmail = function (email) { 87 | if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '') 88 | return true; 89 | 90 | if (email.indexOf('@') !== -1) { 91 | return true; 92 | } else { 93 | loginButtonsSession.errorMessage("Invalid email"); 94 | return false; 95 | } 96 | }; 97 | validatePassword = function (password) { 98 | if (password.length >= 6) { 99 | return true; 100 | } else { 101 | loginButtonsSession.errorMessage("Password must be at least 6 characters long"); 102 | return false; 103 | } 104 | }; 105 | 106 | // 107 | // loginButtonLoggedOut template 108 | // 109 | 110 | Template._loginButtonsLoggedOut.helpers({ 111 | dropdown: dropdown, 112 | services: getLoginServices, 113 | singleService: function () { 114 | var services = getLoginServices(); 115 | if (services.length !== 1) 116 | throw new Error( 117 | "Shouldn't be rendering this template with more than one configured service"); 118 | return services[0]; 119 | }, 120 | configurationLoaded: function () { 121 | return Accounts.loginServicesConfigured(); 122 | } 123 | }); 124 | 125 | 126 | // 127 | // loginButtonsLoggedIn template 128 | // 129 | 130 | // decide whether we should show a dropdown rather than a row of 131 | // buttons 132 | Template._loginButtonsLoggedIn.helpers({ 133 | dropdown: dropdown 134 | }); 135 | 136 | 137 | // 138 | // loginButtonsLoggedInSingleLogoutButton template 139 | // 140 | Template._loginButtonsLoggedInSingleLogoutButton.helpers({ 141 | displayName: displayName 142 | }); 143 | 144 | 145 | 146 | // 147 | // loginButtonsMessageMenuItem helpers 148 | // 149 | Template._loginButtonsMessagesMenuItem.helpers({ 150 | hasMessages: function() { 151 | var hasMessages = false, 152 | errorMessage = loginButtonsSession.get('errorMessage'), 153 | infoMessage = loginButtonsSession.get('infoMessage'); 154 | if (errorMessage && errorMessage != '') 155 | hasMessages = true; 156 | if (infoMessage && infoMessage != '') 157 | hasMessages = true; 158 | return hasMessages; 159 | } 160 | }); 161 | 162 | Template._loginButtonsMessages.onRendered(function() { 163 | Meteor.setTimeout(function () { 164 | $(".temp").fadeOut( "slow" ).remove(); 165 | }, 5000); 166 | }); 167 | 168 | 169 | // loginButtonsMessage (combined) 170 | 171 | Template._loginButtonsMessages.helpers({ 172 | errorMessage: function() { 173 | return loginButtonsSession.get('errorMessage'); 174 | }, 175 | infoMessage: function() { 176 | return loginButtonsSession.get('infoMessage'); 177 | } 178 | }); 179 | 180 | 181 | // ** below doesn't seem to be the issue with the password changed success message 182 | 183 | // Let's remove the messages when clicked. 184 | Template._loginButtonsMessages.onRendered(function() { 185 | $(".message").click(function () { 186 | $(this).fadeOut( "slow" ); 187 | }); 188 | }); 189 | 190 | 191 | // 192 | // loginButtonsLoggingInPadding template 193 | // 194 | 195 | Template._loginButtonsLoggingInPadding.helpers({ 196 | dropdown: dropdown 197 | }); 198 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'iandouglas:accounts-ui-semantic-ui', 3 | version: '1.2.2', 4 | summary: 'Semantic UI styled accounts-ui', 5 | git: 'https://github.com/SharpenedSpoon/accounts-ui-semantic-ui', 6 | documentation: 'README.md' 7 | }); 8 | 9 | 10 | Package.onUse(function(api) { 11 | api.versionsFrom('1.0.4.1'); 12 | api.use(['tracker', 'service-configuration', 'accounts-base', 'underscore', 'templating', 'session'], 'client'); 13 | 14 | // Export Accounts (etc) to packages using this one. 15 | api.imply('accounts-base', ['client', 'server']); 16 | 17 | // Allow us to call Accounts.oauth.serviceNames, if there are any OAuth 18 | // services. 19 | api.use('accounts-oauth', {weak: true}); 20 | // Allow us to directly test if accounts-password (which doesn't use 21 | // Accounts.oauth.registerService) exists. 22 | api.use('accounts-password', {weak: true}); 23 | 24 | api.addFiles([ 25 | 'accounts-ui-semantic-ui.js', 26 | 27 | 'login-buttons.html', 28 | 'login-buttons-single.html', 29 | 'login-buttons-dropdown.html', 30 | 'login-buttons-dialogs.html', 31 | 32 | 'login-buttons-session.js', 33 | 34 | 'login-buttons.js', 35 | 'login-buttons-single.js', 36 | 'login-buttons-dropdown.js', 37 | 'login-buttons-dialogs.js' 38 | ], 'client'); 39 | }); 40 | 41 | 42 | Package.onTest(function(api) { 43 | api.use('tinytest'); 44 | api.use('iandouglas:accounts-ui-semantic-ui'); 45 | api.addFiles('accounts-ui-semantic-ui-tests.js'); 46 | }); 47 | --------------------------------------------------------------------------------