UX: introduces icon-picker component for badges (#8844)

This commit is contained in:
Joffrey JAFFEUX 2020-02-05 00:41:10 +01:00 committed by GitHub
parent 241d8f6452
commit f0fe2ba9ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 171 additions and 12 deletions

View File

@ -11,7 +11,15 @@
<div> <div>
<label for="icon">{{i18n 'admin.badges.icon'}}</label> <label for="icon">{{i18n 'admin.badges.icon'}}</label>
{{input type="text" name="icon" value=buffered.icon}} {{icon-picker
name="icon"
value=buffered.icon
options=(hash
maximum=1
)
onChange=(action (mut buffered.icon))
}}
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p> <p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
</div> </div>

View File

@ -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(
`<div class="${holder}" style='display: none;'></div>`
);
if (!$(`${spriteEl} symbol#${strippedIconName}`).length) {
$(`${spriteEl} .${holder}`).append(
`<svg xmlns='http://www.w3.org/2000/svg'>${icon.symbol}</svg>`
);
}
}
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);
}
}
});

View File

@ -80,10 +80,11 @@ export default SelectKitComponent.extend({
}, },
selectedContent: computed("value.[]", "content.[]", function() { selectedContent: computed("value.[]", "content.[]", function() {
if (this.value && this.value.length) { const value = Ember.makeArray(this.value);
if (value.length) {
let content = []; let content = [];
this.value.forEach(v => { value.forEach(v => {
if (this.selectKit.valueProperty) { if (this.selectKit.valueProperty) {
const c = makeArray(this.content).findBy( const c = makeArray(this.content).findBy(
this.selectKit.valueProperty, this.selectKit.valueProperty,

View File

@ -74,7 +74,10 @@ export default Component.extend(UtilsMixin, {
// Enter // Enter
if (event.keyCode === 13 && this.selectKit.highlighted) { 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; return false;
} }
@ -86,7 +89,10 @@ export default Component.extend(UtilsMixin, {
// Tab // Tab
if (event.keyCode === 9) { if (event.keyCode === 9) {
if (this.selectKit.highlighted && this.selectKit.isExpanded) { 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); this.selectKit.close(event);
return; return;

View File

@ -89,7 +89,10 @@ export default Component.extend(UtilsMixin, {
// Enter // Enter
if (this.selectKit.isExpanded) { if (this.selectKit.isExpanded) {
if (this.selectKit.highlighted) { 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; return false;
} }
} else { } else {
@ -127,7 +130,10 @@ export default Component.extend(UtilsMixin, {
} else if (event.keyCode === 9) { } else if (event.keyCode === 9) {
// Tab // Tab
if (this.selectKit.highlighted && this.selectKit.isExpanded) { 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); this.selectKit.close(event);
} else if ( } else if (

View File

@ -1,5 +1,5 @@
{{#each icons as |icon|}} {{#each icons as |icon|}}
{{d-icon icon title=(dasherize title)}} {{d-icon icon translatedtitle=(dasherize title)}}
{{/each}} {{/each}}
<span class="name"> <span class="name">

View File

@ -24,6 +24,7 @@
@import "common/select-kit/single-select"; @import "common/select-kit/single-select";
@import "common/select-kit/tag-chooser"; @import "common/select-kit/tag-chooser";
@import "common/select-kit/tag-drop"; @import "common/select-kit/tag-drop";
@import "common/select-kit/icon-picker";
@import "common/select-kit/toolbar-popup-menu-options"; @import "common/select-kit/toolbar-popup-menu-options";
@import "common/select-kit/topic-notifications-button"; @import "common/select-kit/topic-notifications-button";
@import "common/select-kit/user-notifications-dropdown"; @import "common/select-kit/user-notifications-dropdown";

View File

@ -75,6 +75,10 @@
margin-right: 5px; margin-right: 5px;
} }
} }
.icon-picker {
width: 350px;
}
} }
.form-horizontal { .form-horizontal {
.ace-wrapper { .ace-wrapper {

View File

@ -0,0 +1,9 @@
.select-kit {
&.icon-picker {
.multi-select-header {
.select-kit-selected-name .d-icon {
color: $primary-high;
}
}
}
}

View File

@ -72,6 +72,10 @@
color: inherit; color: inherit;
display: flex; display: flex;
.d-icon + .name {
margin-left: 0.5em;
}
.name { .name {
display: inline-block; display: inline-block;
} }
@ -137,8 +141,9 @@
flex: 1 1 0%; flex: 1 1 0%;
} }
.d-icon { .d-icon + .name,
margin-right: 5px; .svg-icon-title + .name {
margin-left: 0.5em;
} }
&.is-highlighted { &.is-highlighted {

View File

@ -39,4 +39,14 @@ class SvgSpriteController < ApplicationController
end end
end 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 end

View File

@ -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/: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/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 } get "highlight-js/:hostname/:version.js" => "highlight_js#show", constraints: { hostname: /[\w\.-]+/, format: :js }

View File

@ -312,6 +312,26 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL
false false
end 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 # For use in no_ember .html.erb layouts
def self.raw_svg(name) def self.raw_svg(name)
get_set_cache("raw_svg_#{name}") do 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 end
def self.process(icon_name) def self.process(icon_name)
icon_name.strip! icon_name = icon_name.strip
FA_ICON_MAP.each { |k, v| icon_name.sub!(k, v) } FA_ICON_MAP.each { |k, v| icon_name = icon_name.sub(k, v) }
fa4_to_fa5_names[icon_name] || icon_name fa4_to_fa5_names[icon_name] || icon_name
end end

View File

@ -68,4 +68,29 @@ describe SvgSpriteController do
expect(response.body).to include('my-custom-theme-icon') expect(response.body).to include('my-custom-theme-icon')
end end
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 end