diff --git a/assets/javascripts/wizard-custom-lib.js b/assets/javascripts/wizard-custom-lib.js
new file mode 100644
index 00000000..ec39b956
--- /dev/null
+++ b/assets/javascripts/wizard-custom-lib.js
@@ -0,0 +1,4 @@
+//= require discourse/lib/autocomplete
+//= require discourse/lib/utilities
+//= require discourse/lib/offset-calculator
+//= require discourse/lib/lock-on
diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js
index 651d01e4..c5056c8f 100644
--- a/assets/javascripts/wizard-custom.js
+++ b/assets/javascripts/wizard-custom.js
@@ -1,12 +1,20 @@
//= require ./wizard/custom-wizard
+//= require_tree ./wizard/components
//= require_tree ./wizard/controllers
//= require_tree ./wizard/helpers
//= require_tree ./wizard/initializers
+//= require_tree ./wizard/lib
//= require_tree ./wizard/models
//= require_tree ./wizard/routes
//= require_tree ./wizard/templates
+//= require discourse/components/user-selector
+//= require discourse/components/text-field
+//= require discourse/helpers/user-avatar
+
//= require lodash.js
+window.Discourse = {}
window.Wizard = {};
Wizard.SiteSettings = {};
+Discourse.__widget_helpers = {};
diff --git a/assets/javascripts/wizard/components/custom-user-selector.js.es6 b/assets/javascripts/wizard/components/custom-user-selector.js.es6
new file mode 100644
index 00000000..c553bd68
--- /dev/null
+++ b/assets/javascripts/wizard/components/custom-user-selector.js.es6
@@ -0,0 +1,138 @@
+import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
+import { renderAvatar } from 'discourse/helpers/user-avatar';
+import userSearch from '../lib/user-search';
+
+const template = function(params) {
+ const options = params.options;
+ let html = "
";
+
+ if (options.users) {
+ html += "
";
+ };
+
+ html += "
";
+
+ return new Handlebars.SafeString(html).string;
+};
+
+export default Ember.TextField.extend({
+ attributeBindings: ['autofocus', 'maxLength'],
+ autocorrect: false,
+ autocapitalize: false,
+ name: 'user-selector',
+ id: "custom-member-selector",
+
+ @computed("placeholderKey")
+ placeholder(placeholderKey) {
+ return placeholderKey ? I18n.t(placeholderKey) : "";
+ },
+
+ @observes('usernames')
+ _update() {
+ if (this.get('canReceiveUpdates') === 'true')
+ this.didInsertElement({updateData: true});
+ },
+
+ didInsertElement(opts) {
+ this._super();
+ var self = this,
+ selected = [],
+ groups = [],
+ currentUser = this.currentUser,
+ includeMentionableGroups = this.get('includeMentionableGroups') === 'true',
+ includeMessageableGroups = this.get('includeMessageableGroups') === 'true',
+ includeGroups = this.get('includeGroups') === 'true',
+ allowedUsers = this.get('allowedUsers') === 'true';
+
+ function excludedUsernames() {
+ // hack works around some issues with allowAny eventing
+ const usernames = self.get('single') ? [] : selected;
+
+ if (currentUser && self.get('excludeCurrentUser')) {
+ return usernames.concat([currentUser.get('username')]);
+ }
+ return usernames;
+ }
+
+ this.$().val(this.get('usernames')).autocomplete({
+ template,
+ disabled: this.get('disabled'),
+ single: this.get('single'),
+ allowAny: this.get('allowAny'),
+ updateData: (opts && opts.updateData) ? opts.updateData : false,
+
+ dataSource(term) {
+ const termRegex = /[^a-zA-Z0-9_\-\.@\+]/;
+
+ var results = userSearch({
+ term: term.replace(termRegex, ''),
+ topicId: self.get('topicId'),
+ exclude: excludedUsernames(),
+ includeGroups,
+ allowedUsers,
+ includeMentionableGroups,
+ includeMessageableGroups,
+ group: self.get("group")
+ });
+
+ return results;
+ },
+
+ transformComplete(v) {
+ if (v.username || v.name) {
+ if (!v.username) { groups.push(v.name); }
+ return v.username || v.name;
+ } else {
+ var excludes = excludedUsernames();
+ return v.usernames.filter(function(item){
+ return excludes.indexOf(item) === -1;
+ });
+ }
+ },
+
+ onChangeItems(items) {
+ var hasGroups = false;
+ items = items.map(function(i) {
+ if (groups.indexOf(i) > -1) { hasGroups = true; }
+ return i.username ? i.username : i;
+ });
+ self.set('usernames', items.join(","));
+ self.set('hasGroups', hasGroups);
+
+ selected = items;
+ if (self.get('onChangeCallback')) self.sendAction('onChangeCallback');
+ },
+
+ reverseTransform(i) {
+ return { username: i };
+ }
+
+ });
+ },
+
+ willDestroyElement() {
+ this._super();
+ this.$().autocomplete('destroy');
+ },
+
+ // THIS IS A HUGE HACK TO SUPPORT CLEARING THE INPUT
+ @observes('usernames')
+ _clearInput: function() {
+ if (arguments.length > 1) {
+ if (Em.isEmpty(this.get("usernames"))) {
+ this.$().parent().find("a").click();
+ }
+ }
+ }
+
+});
diff --git a/assets/javascripts/wizard/initializers/custom.js.es6 b/assets/javascripts/wizard/initializers/custom.js.es6
index 5c381ca4..0eb864e8 100644
--- a/assets/javascripts/wizard/initializers/custom.js.es6
+++ b/assets/javascripts/wizard/initializers/custom.js.es6
@@ -11,6 +11,12 @@ export default {
const WizardStep = requirejs('wizard/components/wizard-step').default;
const getUrl = requirejs('discourse-common/lib/get-url').default;
const FieldModel = requirejs('wizard/models/wizard-field').default;
+ const autocomplete = requirejs('discourse/lib/autocomplete').default;
+
+ $.fn.autocomplete = autocomplete;
+
+ // this is for discourse/lib/utilities.avatarImg;
+ Discourse.getURLWithCDN = getUrl;
Router.reopen({
rootURL: getUrl('/w/')
diff --git a/assets/javascripts/wizard/lib/user-search.js.es6 b/assets/javascripts/wizard/lib/user-search.js.es6
new file mode 100644
index 00000000..6b242fd4
--- /dev/null
+++ b/assets/javascripts/wizard/lib/user-search.js.es6
@@ -0,0 +1,134 @@
+import { CANCELLED_STATUS } from 'discourse/lib/autocomplete';
+import getUrl from 'discourse-common/lib/get-url';
+
+var cache = {},
+ cacheTopicId,
+ cacheTime,
+ currentTerm,
+ oldSearch;
+
+function performSearch(term, topicId, includeGroups, includeMentionableGroups, includeMessageableGroups, allowedUsers, group, resultsFn) {
+ var cached = cache[term];
+ if (cached) {
+ resultsFn(cached);
+ return;
+ }
+
+ // need to be able to cancel this
+ oldSearch = $.ajax(getUrl('/u/search/users'), {
+ data: { term: term,
+ topic_id: topicId,
+ include_groups: includeGroups,
+ include_mentionable_groups: includeMentionableGroups,
+ include_messageable_groups: includeMessageableGroups,
+ group: group,
+ topic_allowed_users: allowedUsers }
+ });
+
+ var returnVal = CANCELLED_STATUS;
+
+ oldSearch.then(function (r) {
+ cache[term] = r;
+ cacheTime = new Date();
+ // If there is a newer search term, return null
+ if (term === currentTerm) { returnVal = r; }
+
+ }).always(function(){
+ oldSearch = null;
+ resultsFn(returnVal);
+ });
+}
+
+var debouncedSearch = _.debounce(performSearch, 300);
+
+function organizeResults(r, options) {
+ if (r === CANCELLED_STATUS) { return r; }
+
+ var exclude = options.exclude || [],
+ limit = options.limit || 5,
+ users = [],
+ emails = [],
+ groups = [],
+ results = [];
+
+ if (r.users) {
+ r.users.every(function(u) {
+ if (exclude.indexOf(u.username) === -1) {
+ users.push(u);
+ results.push(u);
+ }
+ return results.length <= limit;
+ });
+ }
+
+ if (options.term.match(/@/)) {
+ let e = { username: options.term };
+ emails = [ e ];
+ results.push(e);
+ }
+
+ if (r.groups) {
+ r.groups.every(function(g) {
+ if (results.length > limit && options.term.toLowerCase() !== g.name.toLowerCase()) return false;
+ if (exclude.indexOf(g.name) === -1) {
+ groups.push(g);
+ results.push(g);
+ }
+ return true;
+ });
+ }
+
+ results.users = users;
+ results.emails = emails;
+ results.groups = groups;
+ return results;
+}
+
+
+export default function userSearch(options) {
+ var term = options.term || "",
+ includeGroups = options.includeGroups,
+ includeMentionableGroups = options.includeMentionableGroups,
+ includeMessageableGroups = options.includeMessageableGroups,
+ allowedUsers = options.allowedUsers,
+ topicId = options.topicId,
+ group = options.group;
+
+
+ if (oldSearch) {
+ oldSearch.abort();
+ oldSearch = null;
+ }
+
+ currentTerm = term;
+
+ return new Ember.RSVP.Promise(function(resolve) {
+ // TODO site setting for allowed regex in username
+ if (term.match(/[^\w_\-\.@\+]/)) {
+ resolve([]);
+ return;
+ }
+ if (((new Date() - cacheTime) > 30000) || (cacheTopicId !== topicId)) {
+ cache = {};
+ }
+
+ cacheTopicId = topicId;
+
+ var clearPromise = setTimeout(function(){
+ resolve(CANCELLED_STATUS);
+ }, 5000);
+
+ debouncedSearch(term,
+ topicId,
+ includeGroups,
+ includeMentionableGroups,
+ includeMessageableGroups,
+ allowedUsers,
+ group,
+ function(r) {
+ clearTimeout(clearPromise);
+ resolve(organizeResults(r, options));
+ });
+
+ });
+}
diff --git a/assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs b/assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs
new file mode 100644
index 00000000..dce57902
--- /dev/null
+++ b/assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs
@@ -0,0 +1 @@
+{{input type='checkbox' checked=field.value}}
diff --git a/assets/javascripts/wizard/templates/components/wizard-field-user-selector.hbs b/assets/javascripts/wizard/templates/components/wizard-field-user-selector.hbs
new file mode 100644
index 00000000..0881bede
--- /dev/null
+++ b/assets/javascripts/wizard/templates/components/wizard-field-user-selector.hbs
@@ -0,0 +1 @@
+{{custom-user-selector usernames=field.value placeholderKey=field.placeholder}}
diff --git a/assets/stylesheets/wizard/wizard_custom.scss b/assets/stylesheets/wizard/wizard_custom.scss
index 074a7ada..e53bb496 100644
--- a/assets/stylesheets/wizard/wizard_custom.scss
+++ b/assets/stylesheets/wizard/wizard_custom.scss
@@ -221,6 +221,146 @@
}
}
+.user-selector-field.wizard-field {
+ div.ac-wrap div.item a.remove, .remove-link {
+ margin-left: 4px;
+ font-size: 11px;
+ line-height: 10px;
+ padding: 1.5px 1.5px 1.5px 2.5px;
+ border-radius: 12px;
+ width: 10px;
+ display: inline-block;
+ border: 1px solid #e9e9e9;
+
+ &:hover {
+ background-color: #f2ab9a;
+ border: 1px solid #ec8972;
+ text-decoration: none;
+ color: #e45735;
+ }
+ }
+
+ div.ac-wrap {
+ width: 98.5% !important;
+ overflow: auto;
+ max-height: 150px;
+ background-color: white;
+ border: 1px solid #e9e9e9;
+ padding: 5px 4px 1px 4px;
+
+ div.item {
+ float: left;
+ margin-bottom: 4px;
+ margin-right: 10px;
+
+ span {
+ height: 24px;
+ display: inline-block;
+ line-height: 20px;
+ }
+ }
+
+ .ac-collapsed-button {
+ float: left;
+ border-radius: 20px;
+ position: relative;
+ top: -2px;
+ margin-right: 10px;
+ }
+
+ input[type="text"] {
+ float: left;
+ margin-bottom: 4px;
+ height: 24px;
+ display: block;
+ border: 0;
+ padding: 0;
+ box-shadow: none;
+ }
+ }
+}
+
+img.avatar {
+ border-radius: 50%;
+ vertical-align: middle;
+}
+
+.autocomplete {
+ z-index: 999999;
+ position: absolute;
+ width: 240px;
+ background-color: white;
+ border: 1px solid #e9e9e9;
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+
+ li {
+ .d-users {
+ color: #333;
+ padding: 0 2px;
+ }
+
+ border-bottom: 1px solid #e9e9e9;
+
+ a {
+ padding: 5px;
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 14px;
+ text-decoration: none;
+
+ img {
+ margin-right: 5px;
+ }
+
+ span.username {
+ color: #000;
+ vertical-align: middle;
+ }
+
+ span.name {
+ font-size: 11px;
+ vertical-align: middle;
+ }
+
+ &.selected {
+ background-color: #d1f0ff;
+ }
+
+ &:hover {
+ background-color: #ffffa6;
+ text-decoration: none;
+ }
+ }
+ }
+ }
+}
+
+.checkbox-field {
+ display: inline-block;
+ width: 100%;
+
+ &> label {
+ float: left;
+ }
+
+ &> .input-area {
+ float: right;
+ margin: 0 20px !important;
+ padding: 10px 0;
+
+ input {
+ cursor: pointer;
+ transform: scale(1.3);
+ }
+ }
+}
+
@keyframes rotate-forever {
0% {
transform: rotate(0deg);
diff --git a/lib/field.rb b/lib/field.rb
index 606e88b2..385e9f76 100644
--- a/lib/field.rb
+++ b/lib/field.rb
@@ -1,6 +1,6 @@
class CustomWizard::Field
def self.types
- @types ||= ['text', 'textarea', 'dropdown', 'image', 'radio']
+ @types ||= ['text', 'textarea', 'dropdown', 'image', 'checkbox', 'user-selector']
end
def self.require_assets
diff --git a/views/layouts/wizard.html.erb b/views/layouts/wizard.html.erb
index e46d157c..e153196b 100644
--- a/views/layouts/wizard.html.erb
+++ b/views/layouts/wizard.html.erb
@@ -5,8 +5,10 @@
<%= preload_script "ember_jquery" %>
<%= preload_script "wizard-vendor" %>
<%= preload_script "wizard-application" %>
+ <%= preload_script "wizard-custom-lib" %>
<%= preload_script "wizard-custom" %>
<%= preload_script "wizard-plugin" %>
+ <%= preload_script "pretty-text-bundle" %>
<%= preload_script "locales/#{I18n.locale}" %>
<%= render partial: "common/special_font_face" %>