mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 02:50:00 +08:00
UX: introduces icon-picker component for badges (#8844)
This commit is contained in:
parent
241d8f6452
commit
f0fe2ba9ac
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -75,6 +75,10 @@
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-picker {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.form-horizontal {
|
.form-horizontal {
|
||||||
.ace-wrapper {
|
.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;
|
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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user