mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 20:36:39 +08:00
UX: introduces icon-picker component for badges (#8844)
This commit is contained in:
parent
241d8f6452
commit
f0fe2ba9ac
|
@ -11,7 +11,15 @@
|
|||
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{#each icons as |icon|}}
|
||||
{{d-icon icon title=(dasherize title)}}
|
||||
{{d-icon icon translatedtitle=(dasherize title)}}
|
||||
{{/each}}
|
||||
|
||||
<span class="name">
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -75,6 +75,10 @@
|
|||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-picker {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
.form-horizontal {
|
||||
.ace-wrapper {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
.select-kit {
|
||||
&.icon-picker {
|
||||
.multi-select-header {
|
||||
.select-kit-selected-name .d-icon {
|
||||
color: $primary-high;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user