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( + `${icon.symbol}` + ); + } + } + + 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