├── .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 |
3 | {{> _resetPasswordDialog}}
4 | {{> _justResetPasswordDialog}}
5 | {{> _enrollAccountDialog}}
6 | {{> _justVerifiedEmailDialog}}
7 | {{> _configureLoginServiceDialog}}
8 | {{> _configureLoginOnDesktopDialog}}
9 |
10 | {{! if we're not showing a dropdown, we need some other place to show messages }}
11 | {{> _loginButtonsMessagesDialog}}
12 |
72 |
73 |
74 |
75 | {{#unless dropdown}}
76 | {{! invisible div used for correct height of surrounding div. this ensures
77 | that the _loginButtons template is always the same height
78 | and the rest of the page doesn't move up and down }}
79 |
80 |
81 |
82 | {{else}}
83 | {{! just add some padding }}
84 |
85 | {{/unless}}
86 |
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 |
--------------------------------------------------------------------------------