diff --git a/app/assets/javascripts/admin/components/list_setting_component.js b/app/assets/javascripts/admin/components/list_setting_component.js
new file mode 100644
index 00000000000..9f39d46ce6f
--- /dev/null
+++ b/app/assets/javascripts/admin/components/list_setting_component.js
@@ -0,0 +1,82 @@
+/**
+ Provide a nice GUI for a pipe-delimited list in the site settings.
+
+ @param settingValue is a reference to SiteSetting.value.
+
+ @class Discourse.ListSettingComponent
+ @extends Ember.Component
+ @namespace Discourse
+ @module Discourse
+ **/
+Discourse.ListSettingComponent = Ember.Component.extend({
+ layoutName: 'components/list-setting',
+
+ init: function() {
+ this._super();
+ this.on("focusOut", this.uncacheValue);
+ this.set('children', []);
+ },
+
+ canAddNew: true,
+
+ readValues: function() {
+ return this.get('settingValue').split('|');
+ }.property('settingValue'),
+
+ /**
+ Transfer the debounced value into the settingValue parameter.
+
+ This will cause a redraw of the child textboxes.
+
+ @param newFocus {Number|undefined} Which list index to focus on next, or undefined to not refocus
+ **/
+ uncacheValue: function(newFocus) {
+ var oldValue = this.get('settingValue'),
+ newValue = this.get('settingValueCached'),
+ self = this;
+
+ if (newValue !== undefined && newValue !== oldValue) {
+ this.set('settingValue', newValue);
+ }
+
+ if (newFocus !== undefined && newFocus > 0) {
+ Em.run.schedule('afterRender', function() {
+ var children = self.get('children');
+ if (newFocus < children.length) {
+ $(children[newFocus].get('element')).focus();
+ } else if (newFocus === children.length) {
+ $(self.get('element')).children().children('.list-add-value').focus();
+ }
+ });
+ }
+ },
+
+ setItemValue: function(index, item) {
+ var values = this.get('readValues');
+ values[index] = item;
+
+ // Remove blank items
+ values = values.filter(function(s) { return s !== ''; });
+ this.setProperties({
+ settingValueCached: values.join('|'),
+ canAddNew: true
+ });
+ },
+
+ actions: {
+ addNewItem: function() {
+ var newValue = this.get('settingValue') + '|';
+ this.setProperties({
+ settingValue: newValue,
+ settingValueCached: newValue,
+ canAddNew: false
+ });
+
+ var self = this;
+ Em.run.schedule('afterRender', function() {
+ var children = self.get('children');
+ $(children[children.length - 1].get('element')).focus();
+ });
+ }
+ }
+});
diff --git a/app/assets/javascripts/admin/components/list_setting_item_component.js b/app/assets/javascripts/admin/components/list_setting_item_component.js
new file mode 100644
index 00000000000..fac5f339b87
--- /dev/null
+++ b/app/assets/javascripts/admin/components/list_setting_item_component.js
@@ -0,0 +1,44 @@
+/**
+ One item in a ListSetting.
+
+ @param parent is the ListSettingComponent.
+
+ @class Discourse.ListSettingItemComponent
+ @extends Ember.Component, Ember.TextSupport
+ @namespace Discourse
+ @module Discourse
+ **/
+Discourse.ListSettingItemComponent = Ember.Component.extend(Ember.TextSupport, {
+ classNames: ['ember-text-field'],
+ tagName: "input",
+ attributeBindings: ['type', 'value', 'size', 'pattern'],
+
+ _initialize: function() {
+ // _parentView is the #each
+ // parent is the ListSettingComponent
+ this.setProperties({
+ value: this.get('_parentView.content'),
+ index: this.get('_parentView.contentIndex')
+ });
+ this.get('parent').get('children')[this.get('index')] = this;
+ }.on('init'),
+
+ markTab: function(e) {
+ var keyCode = e.keyCode || e.which;
+
+ if (keyCode === 9) {
+ this.set('nextIndex', this.get('index') + (e.shiftKey ? -1 : 1));
+ }
+ }.on('keyDown'),
+
+ reloadList: function() {
+ var nextIndex = this.get('nextIndex');
+ this.set('nextIndex', undefined); // one use only
+ this.get('parent').uncacheValue(nextIndex);
+ }.on('focusOut'),
+
+ _elementValueDidChange: function() {
+ this._super();
+ this.get('parent').setItemValue(this.get('index'), this.get('value'));
+ }
+});
diff --git a/app/assets/javascripts/admin/templates/site_settings/setting_list.js.handlebars b/app/assets/javascripts/admin/templates/site_settings/setting_list.js.handlebars
new file mode 100644
index 00000000000..d7423ea8107
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/site_settings/setting_list.js.handlebars
@@ -0,0 +1,17 @@
+
+
{{unbound setting}}
+
+
+ {{list-setting settingValue=value}}
+
{{unbound description}}
+
+{{#if dirty}}
+
+
+
+
+{{else}}
+ {{#if overridden}}
+
+ {{/if}}
+{{/if}}
diff --git a/app/assets/javascripts/admin/views/site_setting_view.js b/app/assets/javascripts/admin/views/site_setting_view.js
index 5e5c05dca75..b288d36c96d 100644
--- a/app/assets/javascripts/admin/views/site_setting_view.js
+++ b/app/assets/javascripts/admin/views/site_setting_view.js
@@ -10,13 +10,15 @@ Discourse.SiteSettingView = Discourse.View.extend(Discourse.ScrollTop, {
classNameBindings: [':row', ':setting', 'content.overridden'],
templateName: function() {
-
- // If we're editing a boolean, return a different template
+ // If we're editing a boolean, show a checkbox
if (this.get('content.type') === 'bool') return 'admin/templates/site_settings/setting_bool';
// If we're editing an enum field, show a dropdown
if (this.get('content.type') === 'enum' ) return 'admin/templates/site_settings/setting_enum';
+ // If we're editing a list, show a list editor
+ if (this.get('content.type') === 'list' ) return 'admin/templates/site_settings/setting_list';
+
// Default to string editor
return 'admin/templates/site_settings/setting_string';
diff --git a/app/assets/javascripts/discourse/templates/components/list-setting.js.handlebars b/app/assets/javascripts/discourse/templates/components/list-setting.js.handlebars
new file mode 100644
index 00000000000..ddf8dcabe39
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/list-setting.js.handlebars
@@ -0,0 +1,8 @@
+
+ {{#each readValues}}
+ {{list-setting-item parent=view action=update classNames="list-input-item"}}
+ {{/each}}
+ {{#if canAddNew}}
+
+ {{/if}}
+
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index be08fea4f66..8010dcf0a42 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -194,6 +194,40 @@
@include medium-width { width: 314px; }
@include small-width { width: 284px; }
}
+ .input-setting-list {
+ width: 408px;
+ padding: 1px;
+ background-color: white;
+ border: 1px solid #e6e6e6;
+ border-radius: 3px;
+ box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3);
+ -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+ transition: border linear 0.2s, box-shadow linear 0.2s;
+
+ .list-input-item {
+ width: 90px;
+ margin: 2px 1px;
+ background-color: white;
+ border: 1px solid #e6e6e6;
+ border-radius: 3px;
+ box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3);
+ -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+ transition: border linear 0.2s, box-shadow linear 0.2s;
+
+ &:focus {
+ border-color: #00aaff;
+ outline: 0;
+ box-shadow: inset 0 1px 1px rgba(51, 51, 51, 0.3), 0 0 8px #00aaff;
+ }
+ }
+
+ .btn.list-add-value {
+ margin: 0px 3px;
+ padding: 4px 10px;
+ color: $link-color;
+ }
+ }
+
.desc {
padding-top: 3px;
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 44d96719b4c..124128a0e26 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -53,15 +53,19 @@ basic:
top_menu:
client: true
refresh: true
+ list: true
default: 'latest|new|unread|starred|top|categories'
post_menu:
client: true
+ list: true
default: 'like|edit|flag|delete|share|bookmark|reply'
share_links:
client: true
+ list: true
default: 'twitter|facebook|google+|email'
category_colors:
client: true
+ list: true
default: 'BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890'
enable_mobile_theme:
client: true
@@ -276,6 +280,7 @@ files:
client: true
default: '.jpg|.jpeg|.png|.gif'
refresh: true
+ list: true
crawl_images:
default:
test: false
@@ -340,9 +345,15 @@ security:
spam:
add_rel_nofollow_to_user_content: true
- exclude_rel_nofollow_domains: ''
- email_domains_blacklist: 'mailinator.com'
- email_domains_whitelist: ''
+ exclude_rel_nofollow_domains:
+ default: ''
+ list: true
+ email_domains_blacklist:
+ default: 'mailinator.com'
+ list: true
+ email_domains_whitelist:
+ default: ''
+ list: true
flags_required_to_hide_post: 3
cooldown_minutes_after_hiding_posts: 10
num_flags_to_block_new_user: 3
@@ -350,7 +361,9 @@ spam:
notify_mods_when_user_blocked: false
flag_sockpuppets: true
newuser_spam_host_threshold: 3
- white_listed_spam_host_domains: ""
+ white_listed_spam_host_domains:
+ default: ''
+ list: true
rate_limits:
unique_posts_mins:
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index 8c19f3a1271..1975c5513d9 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -14,7 +14,7 @@ module SiteSettingExtension
end
def types
- @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum)
+ @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum, :list)
end
def mutex
@@ -38,6 +38,10 @@ module SiteSettingExtension
@enums ||= {}
end
+ def lists
+ @lists ||= []
+ end
+
def hidden_settings
@hidden_settings ||= []
end
@@ -56,10 +60,13 @@ module SiteSettingExtension
enum = opts[:enum]
enums[name] = enum.is_a?(String) ? enum.constantize : enum
end
- if opts[:hidden] == true
+ if opts[:list]
+ lists << name
+ end
+ if opts[:hidden]
hidden_settings << name
end
- if opts[:refresh] == true
+ if opts[:refresh]
refresh_settings << name
end
@@ -261,6 +268,7 @@ module SiteSettingExtension
def get_data_type(name,val)
return types[:null] if val.nil?
return types[:enum] if enums[name]
+ return types[:list] if lists.include? name
case val
when String
@@ -278,12 +286,14 @@ module SiteSettingExtension
case type
when types[:fixnum]
value.to_i
- when types[:string], types[:enum]
+ when types[:string], types[:list], types[:enum]
value
when types[:bool]
value == true || value == "t" || value == "true"
when types[:null]
nil
+ else
+ raise ArgumentError.new :type
end
end