Quellcode durchsuchen

Add user-selector and checkbox as field options

Angus McLeod vor 7 Jahren
Ursprung
Commit
e731ecba23

+ 4 - 0
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

+ 8 - 0
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 = {};

+ 138 - 0
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 = "<div class='autocomplete'>";
+
+  if (options.users) {
+    html += "<ul>";
+    options.users.forEach((u) => {
+      html += `<li><a href title="${u.name}">`;
+      html += renderAvatar(u, { imageSize: 'tiny' });
+      html += `<span class='username'>${u.username}</span>`;
+      if (u.name) {
+        html += `<span class='name'>${u.name}</span>`;
+      }
+      html += `</a></li>`;
+    });
+    html += "</ul>";
+  };
+
+  html += "</div>";
+
+  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();
+      }
+    }
+  }
+
+});

+ 6 - 0
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/')

+ 134 - 0
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));
+        });
+
+  });
+}

+ 1 - 0
assets/javascripts/wizard/templates/components/wizard-field-checkbox.hbs

@@ -0,0 +1 @@
+{{input type='checkbox' checked=field.value}}

+ 1 - 0
assets/javascripts/wizard/templates/components/wizard-field-user-selector.hbs

@@ -0,0 +1 @@
+{{custom-user-selector usernames=field.value placeholderKey=field.placeholder}}

+ 140 - 0
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);

+ 1 - 1
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

+ 2 - 0
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" %>
     <script src="<%= Discourse.base_uri %>/extra-locales/wizard"></script>