refactors select-box

Note: this commit also now uses select-box for mobile topics controls
This commit is contained in:
Joffrey JAFFEUX 2017-08-16 00:41:56 +02:00 committed by GitHub
parent e50cad3338
commit cad6107624
10 changed files with 178 additions and 156 deletions

View File

@ -36,7 +36,7 @@
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3> <h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p> <p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
<p>{{d-select-box data=colorSchemes <p>{{d-select-box content=colorSchemes
textKey="name" textKey="name"
filterable=true filterable=true
value=colorSchemeId value=colorSchemeId

View File

@ -1,4 +1,4 @@
import { observes } from "ember-addons/ember-computed-decorators"; import { on, observes } from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: "select-box", classNames: "select-box",
@ -15,10 +15,11 @@ export default Ember.Component.extend({
caretUpIcon: "caret-up", caretUpIcon: "caret-up",
caretDownIcon: "caret-down", caretDownIcon: "caret-down",
headerText: null,
icon: null, icon: null,
value: null, value: null,
noDataText: I18n.t("select_box.no_data"), noContentText: I18n.t("select_box.no_content"),
lastHoveredId: null, lastHoveredId: null,
idKey: "id", idKey: "id",
@ -40,116 +41,97 @@ export default Ember.Component.extend({
verticalOffset: 0, verticalOffset: 0,
horizontalOffset: 0, horizontalOffset: 0,
_renderBody: false, renderBody: false,
init() { init() {
this._super(); this._super();
if(!this.get("data")) { if (!this.get("content")) {
this.set("data", []); this.set("content", []);
} }
this.setProperties({ this.setProperties({
componentId: this.elementId, componentId: this.elementId,
filteredData: [], filteredContent: [],
selectedData: {} selectedContent: {}
}); });
}, },
@observes("value")
_valueChanged: function() {
if (Ember.isNone(this.get("value"))) {
this.set("lastHoveredId", null);
}
this.set("filteredContent", this._remapContent(this.get("content")));
},
@observes("filter") @observes("filter")
_filter: function() { _filterChanged: function() {
if(_.isEmpty(this.get("filter"))) { if (Ember.isEmpty(this.get("filter"))) {
this.set("filteredData", this._remapData(this.get("data"))); this.set("filteredContent", this._remapContent(this.get("content")));
} else { } else {
const filtered = _.filter(this.get("data"), (data)=> { const filtered = _.filter(this.get("content"), (content) => {
return data[this.get("textKey")].toLowerCase().indexOf(this.get("filter")) > -1; return content[this.get("textKey")].toLowerCase().indexOf(this.get("filter")) > -1;
}); });
this.set("filteredData", this._remapData(filtered)); this.set("filteredContent", this._remapContent(filtered));
} }
}, },
@observes("expanded", "filteredData") @observes("expanded")
_expand: function() { _expandedChanged: function() {
if(this.get("expanded")) { if (this.get("expanded")) {
this.setProperties({focused: false, _renderBody: true}); this.setProperties({ focused: false, renderBody: true });
Ember.$(document).on("keydown.select-box", (event) => { if (Ember.isNone(this.get("lastHoveredId"))) {
const keyCode = event.keyCode || event.which;
if (keyCode === 9) {
this.set("expanded", false);
}
});
if(_.isUndefined(this.get("lastHoveredId"))) {
this.set("lastHoveredId", this.get("value")); this.set("lastHoveredId", this.get("value"));
} }
Ember.run.scheduleOnce("afterRender", this, () => {
this.$(".select-box-filter .filter-query").focus();
this.$(".select-box-collection").css("max-height", this.get("maxCollectionHeight"));
this.$().removeClass("is-reversed");
const offsetTop = this.$()[0].getBoundingClientRect().top;
const windowHeight = Ember.$(window).height();
const headerHeight = this.$(".select-box-header").outerHeight();
const filterHeight = this.$(".select-box-filter").outerHeight();
if(windowHeight - (offsetTop + this.get("maxCollectionHeight") + filterHeight + headerHeight) < 0) {
this.$().addClass("is-reversed");
this.$(".select-box-body").css({
left: this.get("horizontalOffset"),
top: "",
bottom: headerHeight + this.get("verticalOffset")
});
} else {
this.$(".select-box-body").css({
left: this.get("horizontalOffset"),
top: headerHeight + this.get("verticalOffset"),
bottom: ""
});
}
this.$(".select-box-wrapper").css({
width: this.get("maxWidth"),
display: "block",
height: headerHeight + this.$(".select-box-body").outerHeight()
});
});
} else {
Ember.$(document).off("keydown.select-box");
this.$(".select-box-wrapper").hide();
}; };
}, },
willDestroyElement() { @on("willDestroyElement")
this._super(); _unbindEvents: function() {
$(document).off("click.select-box");
Ember.$(document).off("click.select-box"); $(document).off("keydown.select-box");
Ember.$(document).off("keydown.select-box");
this.$(".select-box-offscreen").off("focusin.select-box"); this.$(".select-box-offscreen").off("focusin.select-box");
this.$(".select-box-offscreen").off("focusout.select-box"); this.$(".select-box-offscreen").off("focusout.select-box");
}, },
didReceiveAttrs() { @on("didRender")
this._super(); _configureSelectBoxDOM: function() {
if (this.get("expanded")) {
this.$(".select-box-body").css('width', this.get("maxWidth"));
this.$(".select-box-filter .filter-query").focus();
this.$(".select-box-collection").css("max-height", this.get("maxCollectionHeight"));
this.set("lastHoveredId", this.get("data")[this.get("idKey")]); this._bindTab();
this.set("filteredData", this._remapData(this.get("data"))); this._applyDirection();
this._setSelectedData(this.get("data")); this._positionSelectBoxWrapper();
} else {
$(document).off("keydown.select-box");
this.$(".select-box-wrapper").hide();
}
}, },
didRender() { @observes("content.[]")
this._super(); @on("didReceiveAttrs")
_contentChanged: function() {
if (!Ember.isNone(this.get("value"))) {
this.set("lastHoveredId", this.get("content")[this.get("idKey")]);
} else {
this.set("lastHoveredId", null);
}
this.$(".select-box-body").css('width', this.get("maxWidth")); this.set("filteredContent", this._remapContent(this.get("content")));
this._expand(); this._setSelectedContent(this.get("content"));
this.set("headerText", this.get("defaultHeaderText") || this.get("selectedContent.text"));
}, },
didInsertElement() { @on("didInsertElement")
this._super(); _bindEvents: function() {
$(document).on("click.select-box", (event) => {
Ember.$(document).on("click.select-box", (event) => { const clickOutside = $(event.target).parents(".select-box").attr("id") !== this.$().attr("id");
if(this.get("expanded") && $(event.target).parents(".select-box").attr("id") !== this.$().attr("id")) { if (this.get("expanded") && clickOutside) {
this.setProperties({ this.setProperties({
expanded: false, expanded: false,
focused: false focused: false
@ -187,25 +169,68 @@ export default Ember.Component.extend({
} }
}, },
_setSelectedData(data) { _setSelectedContent(content) {
const selectedData = _.find(data, (d)=> { const selectedContent = content.find((c) => {
return d[this.get("idKey")] === this.get("value"); return c[this.get("idKey")] === this.get("value");
}); });
if(!_.isUndefined(selectedData)) { if (!Ember.isNone(selectedContent)) {
this.set("selectedData", this._normalizeData(selectedData)); this.set("selectedContent", this._normalizeContent(selectedContent));
} }
}, },
_remapData(data) { _remapContent(content) {
return data.map(d => this._normalizeData(d)); return content.map(c => this._normalizeContent(c));
}, },
_normalizeData(data) { _normalizeContent(content) {
return { return {
id: data[this.get("idKey")], id: content[this.get("idKey")],
text: data[this.get("textKey")], text: content[this.get("textKey")],
icon: data[this.get("iconKey")] icon: content[this.get("iconKey")]
}; };
}, },
_bindTab() {
$(document).on("keydown.select-box", (event) => {
const keyCode = event.keyCode || event.which;
if (keyCode === 9) {
this.set("expanded", false);
}
});
},
_positionSelectBoxWrapper() {
const headerHeight = this.$(".select-box-header").outerHeight();
this.$(".select-box-wrapper").css({
width: this.get("maxWidth"),
display: "block",
height: headerHeight + this.$(".select-box-body").outerHeight()
});
},
_applyDirection() {
this.$().removeClass("is-reversed");
const offsetTop = this.$()[0].getBoundingClientRect().top;
const windowHeight = $(window).height();
const headerHeight = this.$(".select-box-header").outerHeight();
const filterHeight = this.$(".select-box-filter").outerHeight();
if (windowHeight - (offsetTop + this.get("maxCollectionHeight") + filterHeight + headerHeight) < 0) {
this.$().addClass("is-reversed");
this.$(".select-box-body").css({
left: this.get("horizontalOffset"),
top: "",
bottom: headerHeight + this.get("verticalOffset")
});
} else {
this.$(".select-box-body").css({
left: this.get("horizontalOffset"),
top: headerHeight + this.get("verticalOffset"),
bottom: ""
});
}
},
}); });

View File

@ -10,25 +10,25 @@ export default Ember.Component.extend({
lastHoveredId: null, lastHoveredId: null,
mouseEnter() { mouseEnter() {
this.sendAction("onHover", this.get("data.id")); this.sendAction("onHover", this.get("content.id"));
}, },
click() { click() {
this.sendAction("onSelect", this.get("data.id")); this.sendAction("onSelect", this.get("content.id"));
}, },
didReceiveAttrs() { didReceiveAttrs() {
this._super(); this._super();
this.set("isHighlighted", this._isHighlighted()); this.set("isHighlighted", this._isHighlighted());
this.set("text", this.get("data.text")); this.set("text", this.get("content.text"));
}, },
_isHighlighted() { _isHighlighted() {
if(_.isUndefined(this.get("lastHoveredId"))) { if(_.isUndefined(this.get("lastHoveredId"))) {
return this.get("data.id") === this.get("selectedId"); return this.get("content.id") === this.get("selectedId");
} else { } else {
return this.get("data.id") === this.get("lastHoveredId"); return this.get("content.id") === this.get("lastHoveredId");
} }
}, },
}); });

View File

@ -1,12 +1,13 @@
import { iconHTML } from 'discourse-common/lib/icon-library';
import Combobox from 'discourse-common/components/combo-box';
import { observes } from 'ember-addons/ember-computed-decorators'; import { observes } from 'ember-addons/ember-computed-decorators';
import DiscourseSelectBoxComponent from "discourse/components/d-select-box";
export default Combobox.extend({ export default DiscourseSelectBoxComponent.extend({
none: "topic.controls",
init() { init() {
this._super(); this._super();
this.set("textKey", "name");
this.set("defaultHeaderText", I18n.t("topic.controls"));
this.set("maxCollectionHeight", 300);
this._createContent(); this._createContent();
}, },
@ -24,23 +25,20 @@ export default Combobox.extend({
} else { } else {
content.push({ id: 'bookmark', icon: 'bookmark', name: I18n.t('bookmarked.title') }); content.push({ id: 'bookmark', icon: 'bookmark', name: I18n.t('bookmarked.title') });
} }
content.push({ id: 'share', icon: 'link', name: I18n.t('topic.share.title') }); content.push({ id: 'share', icon: 'link', name: I18n.t('topic.share.title') });
if (details.get('can_flag_topic')) { if (details.get('can_flag_topic')) {
content.push({ id: 'flag', icon: 'flag', name: I18n.t('topic.flag_topic.title') }); content.push({ id: 'flag', icon: 'flag', name: I18n.t('topic.flag_topic.title') });
} }
this.comboTemplate = (item) => {
const contentItem = content.findBy('id', item.id);
if (!contentItem) { return item.text; }
return `${iconHTML(contentItem.icon)}&nbsp; ${item.text}`;
};
this.set('content', content); this.set('content', content);
}, },
@observes('value') @observes('value')
_valueChanged() { _valueChanged() {
this._super();
const value = this.get('value'); const value = this.get('value');
const topic = this.get('topic'); const topic = this.get('topic');

View File

@ -7,7 +7,7 @@
/> />
{{component selectBoxHeaderComponent {{component selectBoxHeaderComponent
data=selectedData text=headerText
focused=focused focused=focused
caretUpIcon=caretUpIcon caretUpIcon=caretUpIcon
caretDownIcon=caretDownIcon caretDownIcon=caretDownIcon
@ -17,7 +17,7 @@
}} }}
<div class="select-box-body"> <div class="select-box-body">
{{#if _renderBody}} {{#if renderBody}}
{{#if filterable}} {{#if filterable}}
{{component selectBoxFilterComponent {{component selectBoxFilterComponent
onFilterChange=(action "onFilterChange") onFilterChange=(action "onFilterChange")
@ -27,12 +27,12 @@
{{/if}} {{/if}}
{{component selectBoxCollectionComponent {{component selectBoxCollectionComponent
filteredData=filteredData filteredContent=filteredContent
selectBoxRowComponent=selectBoxRowComponent selectBoxRowComponent=selectBoxRowComponent
lastHoveredId=lastHoveredId lastHoveredId=lastHoveredId
onSelectRow=(action "onSelectRow") onSelectRow=(action "onSelectRow")
onHoverRow=(action "onHoverRow") onHoverRow=(action "onHoverRow")
noDataText=noDataText noContentText=noContentText
selectedId=value selectedId=value
}} }}
{{/if}} {{/if}}

View File

@ -1,16 +1,16 @@
<ul class="collection"> <ul class="collection">
{{#each filteredData as |data|}} {{#each filteredContent as |content|}}
{{component selectBoxRowComponent {{component selectBoxRowComponent
data=data content=content
lastHoveredId=lastHoveredId lastHoveredId=lastHoveredId
onSelect=onSelectRow onSelect=onSelectRow
onHover=onHoverRow onHover=onHoverRow
selectedId=selectedId selectedId=selectedId
}} }}
{{else}} {{else}}
{{#if noDataText}} {{#if noContentText}}
<li class="select-box-row no-data"> <li class="select-box-row no-content">
{{noDataText}} {{noContentText}}
</li> </li>
{{/if}} {{/if}}
{{/each}} {{/each}}

View File

@ -6,7 +6,7 @@
{{/if}} {{/if}}
<span class="current-selection"> <span class="current-selection">
{{data.text}} {{text}}
</span> </span>
<div class="caret-icon"> <div class="caret-icon">

View File

@ -1,7 +1,7 @@
{{#if data.icon}} {{#if content.icon}}
{{d-icon data.icon}} {{d-icon content.icon}}
{{/if}} {{/if}}
<p class="text"> <p class="text">
{{data.text}} {{content.text}}
</p> </p>

View File

@ -1144,7 +1144,7 @@ en:
alt: 'Alt' alt: 'Alt'
select_box: select_box:
no_data: No data no_content: No content
filter_placeholder: Search... filter_placeholder: Search...
emoji_picker: emoji_picker:

View File

@ -1,68 +1,69 @@
import componentTest from 'helpers/component-test'; import componentTest from 'helpers/component-test';
moduleForComponent('select-box', {integration: true}); moduleForComponent('select-box', { integration: true });
componentTest('updating the data refreshes the list', { componentTest('updating the content refreshes the list', {
template: '{{select-box value=1 data=data}}', template: '{{select-box value=1 content=content}}',
beforeEach() { beforeEach() {
this.set("data", [{id:1, text:"robin"}]); this.set("content", [{id:1, text:"robin"}]);
}, },
test(assert) { test(assert) {
click(this.$(".select-box-header")); click(this.$(".select-box-header"));
andThen(() => { andThen(() => {
assert.equal(this.$(".select-box-row .text").html().trim(), "robin"); assert.equal(this.$(".select-box-row .text").html().trim(), "robin");
andThen(() => this.set("data", [{id:1, text:"regis"}]));
andThen(() => assert.equal(this.$(".select-box-row .text").html().trim(), "regis"));
}); });
andThen(() => this.set("content", [{id:1, text:"regis"}]));
andThen(() => assert.equal(this.$(".select-box-row .text").html().trim(), "regis"));
} }
}); });
componentTest('accepts a value by reference', { componentTest('accepts a value by reference', {
template: '{{select-box value=value data=data}}', template: '{{select-box value=value content=content}}',
beforeEach() { beforeEach() {
this.set("value", 1); this.set("value", 1);
this.set("data", [{id:1, text:"robin"}, {id: 2, text:"regis"}]); this.set("content", [{id:1, text:"robin"}, {id: 2, text:"regis"}]);
}, },
test(assert) { test(assert) {
click(this.$(".select-box-header")); click(this.$(".select-box-header"));
andThen(() => { andThen(() => {
assert.equal(this.$(".select-box-row.is-highlighted .text").html().trim(), "robin", "it highlights the row corresponding to the value"); assert.equal(this.$(".select-box-row.is-highlighted .text").html().trim(), "robin", "it highlights the row corresponding to the value");
});
andThen(() => {
click(this.$(".select-box-row[title='robin']")); click(this.$(".select-box-row[title='robin']"));
andThen(() => assert.equal(this.get("value"), 1, "it mutates the value")); andThen(() => {
assert.equal(this.get("value"), 1, "it mutates the value");
});
}); });
} }
}); });
componentTest('select-box can be filtered', { componentTest('select-box can be filtered', {
template: '{{select-box filterable=true value=1 data=data}}', template: '{{select-box filterable=true value=1 content=content}}',
beforeEach() { beforeEach() {
this.set("data", [{id:1, text:"robin"}, {id: 2, text:"regis"}]); this.set("content", [{id:1, text:"robin"}, {id: 2, text:"regis"}]);
}, },
test(assert) { test(assert) {
click(this.$(".select-box-header")); click(this.$(".select-box-header"));
andThen(() => assert.equal(this.$(".filter-query").length, 1, "it has a search input"));
andThen(() => { andThen(() => {
andThen(() => assert.equal(this.$(".filter-query").length, 1, "it has a search input")); this.$(".filter-query").val("regis");
this.$(".filter-query").trigger("keyup");
andThen(() => {
this.$(".filter-query").val("regis");
this.$(".filter-query").trigger("keyup");
});
andThen(() => assert.equal(this.$(".select-box-row").length, 1, "it filters results"));
andThen(() => {
this.$(".filter-query").val("");
this.$(".filter-query").trigger("keyup");
});
andThen(() => assert.equal(this.$(".select-box-row").length, 2, "it returns to original data when filter is empty"));
}); });
andThen(() => assert.equal(this.$(".select-box-row").length, 1, "it filters results"));
andThen(() => {
this.$(".filter-query").val("");
this.$(".filter-query").trigger("keyup");
});
andThen(() => assert.equal(this.$(".select-box-row").length, 2, "it returns to original content when filter is empty"));
} }
}); });
@ -129,7 +130,6 @@ componentTest('not filterable by default', {
componentTest('select-box is expandable', { componentTest('select-box is expandable', {
template: '{{select-box}}', template: '{{select-box}}',
test(assert) { test(assert) {
click(".select-box-header"); click(".select-box-header");
andThen(() => { andThen(() => {
assert.equal(this.$(".select-box").hasClass("is-expanded"), true); assert.equal(this.$(".select-box").hasClass("is-expanded"), true);
@ -143,11 +143,11 @@ componentTest('select-box is expandable', {
}); });
componentTest('accepts custom id/text keys', { componentTest('accepts custom id/text keys', {
template: '{{select-box value=value data=data idKey="identifier" textKey="name"}}', template: '{{select-box value=value content=content idKey="identifier" textKey="name"}}',
beforeEach() { beforeEach() {
this.set("value", 1); this.set("value", 1);
this.set("data", [{identifier:1, name:"robin"}]); this.set("content", [{identifier:1, name:"robin"}]);
}, },
test(assert) { test(assert) {
@ -159,10 +159,10 @@ componentTest('accepts custom id/text keys', {
}); });
componentTest('doesn’t render collection content before first expand', { componentTest('doesn’t render collection content before first expand', {
template: '{{select-box value=1 data=data idKey="identifier" textKey="name"}}', template: '{{select-box value=1 content=content idKey="identifier" textKey="name"}}',
beforeEach() { beforeEach() {
this.set("data", [{identifier:1, name:"robin"}]); this.set("content", [{identifier:1, name:"robin"}]);
}, },
test(assert) { test(assert) {
@ -176,10 +176,10 @@ componentTest('doesn’t render collection content before first expand', {
}); });
componentTest('persists filter state when expandind/collapsing', { componentTest('persists filter state when expandind/collapsing', {
template: '{{select-box value=1 data=data filterable=true}}', template: '{{select-box value=1 content=content filterable=true}}',
beforeEach() { beforeEach() {
this.set("data", [{id:1, text:"robin"}, {id:2, text:"régis"}]); this.set("content", [{id:1, text:"robin"}, {id:2, text:"régis"}]);
}, },
test(assert) { test(assert) {
@ -205,12 +205,11 @@ componentTest('persists filter state when expandind/collapsing', {
} }
}); });
componentTest('supports options to limit size', { componentTest('supports options to limit size', {
template: '{{select-box maxWidth=100 maxCollectionHeight=20 data=data}}', template: '{{select-box maxWidth=100 maxCollectionHeight=20 content=content}}',
beforeEach() { beforeEach() {
this.set("data", [{id:1, text:"robin"}]); this.set("content", [{id:1, text:"robin"}]);
}, },
test(assert) { test(assert) {