diff --git a/app/assets/javascripts/admin/templates/badges-show.hbs b/app/assets/javascripts/admin/templates/badges-show.hbs
index b0118260032..3349e5e6f7d 100644
--- a/app/assets/javascripts/admin/templates/badges-show.hbs
+++ b/app/assets/javascripts/admin/templates/badges-show.hbs
@@ -11,7 +11,15 @@
- {{input type="text" name="icon" value=buffered.icon}}
+ {{icon-picker
+ name="icon"
+ value=buffered.icon
+ options=(hash
+ maximum=1
+ )
+ onChange=(action (mut buffered.icon))
+ }}
+
{{i18n 'admin.badges.icon_help'}}
diff --git a/app/assets/javascripts/select-kit/components/icon-picker.js.es6 b/app/assets/javascripts/select-kit/components/icon-picker.js.es6
new file mode 100644
index 00000000000..2c578b74f22
--- /dev/null
+++ b/app/assets/javascripts/select-kit/components/icon-picker.js.es6
@@ -0,0 +1,63 @@
+import MultiSelectComponent from "select-kit/components/multi-select";
+import { computed } from "@ember/object";
+import { ajax } from "discourse/lib/ajax";
+import { makeArray } from "discourse-common/lib/helpers";
+import { convertIconClass } from "discourse-common/lib/icon-library";
+
+export default MultiSelectComponent.extend({
+ pluginApiIdentifiers: ["icon-picker"],
+ classNames: ["icon-picker"],
+
+ content: computed("value.[]", function() {
+ return makeArray(this.value).map(this._processIcon);
+ }),
+
+ search(filter = "") {
+ return ajax("/svg-sprite/picker-search", { data: { filter } }).then(icons =>
+ icons.map(this._processIcon)
+ );
+ },
+
+ _processIcon(icon) {
+ const iconName = typeof icon === "object" ? icon.id : icon,
+ strippedIconName = convertIconClass(iconName);
+
+ const spriteEl = "#svg-sprites",
+ holder = "ajax-icon-holder";
+
+ if (typeof icon === "object") {
+ if ($(`${spriteEl} .${holder}`).length === 0)
+ $(spriteEl).append(
+ ``
+ );
+
+ if (!$(`${spriteEl} symbol#${strippedIconName}`).length) {
+ $(`${spriteEl} .${holder}`).append(
+ ``
+ );
+ }
+ }
+
+ return {
+ id: iconName,
+ name: iconName,
+ icon: strippedIconName
+ };
+ },
+
+ willDestroyElement() {
+ $("#svg-sprites .ajax-icon-holder").remove();
+ this._super(...arguments);
+ },
+
+ actions: {
+ onChange(value, item) {
+ if (this.selectKit.options.maximum === 1) {
+ value = value.length ? value[0] : null;
+ item = item.length ? item[0] : null;
+ }
+
+ this.attrs.onChange && this.attrs.onChange(value, item);
+ }
+ }
+});
diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6
index 3b82012cab7..a15a9e79ea3 100644
--- a/app/assets/javascripts/select-kit/components/multi-select.js.es6
+++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6
@@ -80,10 +80,11 @@ export default SelectKitComponent.extend({
},
selectedContent: computed("value.[]", "content.[]", function() {
- if (this.value && this.value.length) {
+ const value = Ember.makeArray(this.value);
+ if (value.length) {
let content = [];
- this.value.forEach(v => {
+ value.forEach(v => {
if (this.selectKit.valueProperty) {
const c = makeArray(this.content).findBy(
this.selectKit.valueProperty,
diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6
index a86b01378c6..6b21d2183f4 100644
--- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6
+++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-filter.js.es6
@@ -74,7 +74,10 @@ export default Component.extend(UtilsMixin, {
// Enter
if (event.keyCode === 13 && this.selectKit.highlighted) {
- this.selectKit.select(this.getValue(this.selectKit.highlighted));
+ this.selectKit.select(
+ this.getValue(this.selectKit.highlighted),
+ this.selectKit.highlighted
+ );
return false;
}
@@ -86,7 +89,10 @@ export default Component.extend(UtilsMixin, {
// Tab
if (event.keyCode === 9) {
if (this.selectKit.highlighted && this.selectKit.isExpanded) {
- this.selectKit.select(this.getValue(this.selectKit.highlighted));
+ this.selectKit.select(
+ this.getValue(this.selectKit.highlighted),
+ this.selectKit.highlighted
+ );
}
this.selectKit.close(event);
return;
diff --git a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6 b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6
index 4ce36958c2f..faf8f27b7ec 100644
--- a/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6
+++ b/app/assets/javascripts/select-kit/components/select-kit/select-kit-header.js.es6
@@ -89,7 +89,10 @@ export default Component.extend(UtilsMixin, {
// Enter
if (this.selectKit.isExpanded) {
if (this.selectKit.highlighted) {
- this.selectKit.select(this.getValue(this.selectKit.highlighted));
+ this.selectKit.select(
+ this.getValue(this.selectKit.highlighted),
+ this.selectKit.highlighted
+ );
return false;
}
} else {
@@ -127,7 +130,10 @@ export default Component.extend(UtilsMixin, {
} else if (event.keyCode === 9) {
// Tab
if (this.selectKit.highlighted && this.selectKit.isExpanded) {
- this.selectKit.select(this.getValue(this.selectKit.highlighted));
+ this.selectKit.select(
+ this.getValue(this.selectKit.highlighted),
+ this.selectKit.highlighted
+ );
}
this.selectKit.close(event);
} else if (
diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs
index c5cd814c4e9..b27647f8b4b 100644
--- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs
+++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-row.hbs
@@ -1,5 +1,5 @@
{{#each icons as |icon|}}
- {{d-icon icon title=(dasherize title)}}
+ {{d-icon icon translatedtitle=(dasherize title)}}
{{/each}}
diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss
index 42b402d00eb..d7ea24129f3 100644
--- a/app/assets/stylesheets/common.scss
+++ b/app/assets/stylesheets/common.scss
@@ -24,6 +24,7 @@
@import "common/select-kit/single-select";
@import "common/select-kit/tag-chooser";
@import "common/select-kit/tag-drop";
+@import "common/select-kit/icon-picker";
@import "common/select-kit/toolbar-popup-menu-options";
@import "common/select-kit/topic-notifications-button";
@import "common/select-kit/user-notifications-dropdown";
diff --git a/app/assets/stylesheets/common/admin/badges.scss b/app/assets/stylesheets/common/admin/badges.scss
index f7d2b367f29..78d715f407a 100644
--- a/app/assets/stylesheets/common/admin/badges.scss
+++ b/app/assets/stylesheets/common/admin/badges.scss
@@ -75,6 +75,10 @@
margin-right: 5px;
}
}
+
+ .icon-picker {
+ width: 350px;
+ }
}
.form-horizontal {
.ace-wrapper {
diff --git a/app/assets/stylesheets/common/select-kit/icon-picker.scss b/app/assets/stylesheets/common/select-kit/icon-picker.scss
new file mode 100644
index 00000000000..2b726810ee1
--- /dev/null
+++ b/app/assets/stylesheets/common/select-kit/icon-picker.scss
@@ -0,0 +1,9 @@
+.select-kit {
+ &.icon-picker {
+ .multi-select-header {
+ .select-kit-selected-name .d-icon {
+ color: $primary-high;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss
index 433f2d2c9ed..27ec940d3fb 100644
--- a/app/assets/stylesheets/common/select-kit/select-kit.scss
+++ b/app/assets/stylesheets/common/select-kit/select-kit.scss
@@ -72,6 +72,10 @@
color: inherit;
display: flex;
+ .d-icon + .name {
+ margin-left: 0.5em;
+ }
+
.name {
display: inline-block;
}
@@ -137,8 +141,9 @@
flex: 1 1 0%;
}
- .d-icon {
- margin-right: 5px;
+ .d-icon + .name,
+ .svg-icon-title + .name {
+ margin-left: 0.5em;
}
&.is-highlighted {
diff --git a/app/controllers/svg_sprite_controller.rb b/app/controllers/svg_sprite_controller.rb
index a3c5cf458a7..c4cd07e23e2 100644
--- a/app/controllers/svg_sprite_controller.rb
+++ b/app/controllers/svg_sprite_controller.rb
@@ -39,4 +39,14 @@ class SvgSpriteController < ApplicationController
end
end
end
+
+ def icon_picker_search
+ RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do
+ params.permit(:filter)
+ filter = params[:filter] || ""
+
+ icons = SvgSprite.icon_picker_search(filter)
+ render json: icons.take(200), root: false
+ end
+ end
end
diff --git a/config/routes.rb b/config/routes.rb
index c715a4ebe2a..a58805d7941 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -492,6 +492,7 @@ Discourse::Application.routes.draw do
get "svg-sprite/:hostname/svg-:theme_ids-:version.js" => "svg_sprite#show", constraints: { hostname: /[\w\.-]+/, version: /\h{40}/, theme_ids: /([0-9]+(,[0-9]+)*)?/, format: :js }
get "svg-sprite/search/:keyword" => "svg_sprite#search", format: false, constraints: { keyword: /[-a-z0-9\s\%]+/ }
+ get "svg-sprite/picker-search" => "svg_sprite#icon_picker_search", defaults: { format: :json }
get "highlight-js/:hostname/:version.js" => "highlight_js#show", constraints: { hostname: /[\w\.-]+/, format: :js }
diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb
index 33d51ca4844..264154d3a63 100644
--- a/lib/svg_sprite/svg_sprite.rb
+++ b/lib/svg_sprite/svg_sprite.rb
@@ -312,6 +312,26 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
false
end
+ def self.icon_picker_search(keyword)
+ results = Set.new
+
+ sprite_sources([SiteSetting.default_theme_id]).each do |fname|
+ svg_file = Nokogiri::XML(File.open(fname))
+ svg_filename = "#{File.basename(fname, ".svg")}"
+
+ svg_file.css('symbol').each do |sym|
+ icon_id = prepare_symbol(sym, svg_filename)
+ if keyword.empty? || icon_id.include?(keyword)
+ sym.attributes['id'].value = icon_id
+ sym.css('title').each(&:remove)
+ results.add(id: icon_id, symbol: sym.to_xml)
+ end
+ end
+ end
+
+ results.sort_by { |icon| icon[:id] }
+ end
+
# For use in no_ember .html.erb layouts
def self.raw_svg(name)
get_set_cache("raw_svg_#{name}") do
@@ -404,8 +424,8 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
end
def self.process(icon_name)
- icon_name.strip!
- FA_ICON_MAP.each { |k, v| icon_name.sub!(k, v) }
+ icon_name = icon_name.strip
+ FA_ICON_MAP.each { |k, v| icon_name = icon_name.sub(k, v) }
fa4_to_fa5_names[icon_name] || icon_name
end
diff --git a/spec/requests/svg_sprite_controller_spec.rb b/spec/requests/svg_sprite_controller_spec.rb
index 5fb1d9c2924..ca61ee3027d 100644
--- a/spec/requests/svg_sprite_controller_spec.rb
+++ b/spec/requests/svg_sprite_controller_spec.rb
@@ -68,4 +68,29 @@ describe SvgSpriteController do
expect(response.body).to include('my-custom-theme-icon')
end
end
+
+ context 'icon_picker_search' do
+ it 'should work with no filter and max out at 200 results' do
+ user = sign_in(Fabricate(:user))
+ get '/svg-sprite/picker-search'
+
+ expect(response.status).to eq(200)
+
+ data = JSON.parse(response.body)
+ expect(data.length).to eq(200)
+ expect(data[0]["id"]).to eq("ad")
+ end
+
+ it 'should filter' do
+ user = sign_in(Fabricate(:user))
+
+ get '/svg-sprite/picker-search', params: { filter: '500px' }
+
+ expect(response.status).to eq(200)
+
+ data = JSON.parse(response.body)
+ expect(data.length).to eq(1)
+ expect(data[0]["id"]).to eq("fab-500px")
+ end
+ end
end