Skip to content

Commit

Permalink
RelationshipSelect improvements
Browse files Browse the repository at this point in the history
Make CSS available in forum frontend
Add suggestion API
Allow disabling field
  • Loading branch information
clarkwinkelmann committed Nov 10, 2022
1 parent bd9c024 commit 1cc8bff
Show file tree
Hide file tree
Showing 12 changed files with 1,072 additions and 1,071 deletions.
3 changes: 2 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
->route('/extension/{id:[a-zA-Z0-9_-]+}', 'extension'),

(new Extend\Frontend('forum'))
->js(__DIR__ . '/js/dist/forum.js'),
->js(__DIR__ . '/js/dist/forum.js')
->css(__DIR__ . '/resources/less/forum.less'),

new Extend\Locales(__DIR__ . '/resources/locale'),

Expand Down
19 changes: 17 additions & 2 deletions js/dist-typings/common/components/AbstractRelationshipSelect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import KeyboardNavigatable from '../utils/KeyboardNavigatable';
import ItemList from 'flarum/common/utils/ItemList';
export interface RelationshipSelectAttrs<T> extends ComponentAttrs {
relationship: T | T[] | null;
hasOne: boolean;
onchange: (value: T | T[] | null) => {};
hasOne?: boolean;
onchange?: (value: T | T[] | null) => {};
placeholder?: string;
suggest?: T | T[] | (() => Promise<T | T[]>);
disabled?: boolean;
readonly?: boolean;
}
export default abstract class AbstractRelationshipSelect<T extends Model> extends Component<RelationshipSelectAttrs<T>> {
searchFilter: string;
Expand All @@ -18,6 +21,8 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
navigator: KeyboardNavigatable;
dropdownIsFocused: boolean;
onmousedown: (event: Event) => void;
cachedSuggestedResults: T[] | null;
suggestedPromiseLoaded: boolean;
className(): string;
abstract search(query: string): Promise<void>;
abstract results(query: string): T[] | null;
Expand All @@ -42,4 +47,14 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
getDomElement(index: number): JQuery<HTMLElement>;
setIndex(index: number, scrollToItem: boolean): void;
onready(): void;
/**
* Whether the dropdown should open above the field instead of below
*/
directionUp(): boolean;
/**
* Shortcut to be used in the results() implementation to render the suggestions.
* If the suggestion is a function, its resulting Promise will be executed and the Select will redraw once results are available.
* If there are zero suggestions, null will be returned to show the spinner identically to a Select without suggestions.
*/
suggestedResults(): T[] | null;
}
2 changes: 1 addition & 1 deletion js/dist/admin.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/backoffice.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/backoffice.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/forum.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/forum.js.map

Large diffs are not rendered by default.

99 changes: 93 additions & 6 deletions js/src/common/components/AbstractRelationshipSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import ItemList from 'flarum/common/utils/ItemList';

export interface RelationshipSelectAttrs<T> extends ComponentAttrs {
relationship: T | T[] | null
hasOne: boolean
onchange: (value: T | T[] | null) => {},
hasOne?: boolean
onchange?: (value: T | T[] | null) => {},
placeholder?: string
suggest?: T | T[] | (() => Promise<T | T[]>)
disabled?: boolean
readonly?: boolean
}

export default abstract class AbstractRelationshipSelect<T extends Model> extends Component<RelationshipSelectAttrs<T>> {
Expand All @@ -22,6 +25,8 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
navigator!: KeyboardNavigatable;
dropdownIsFocused: boolean = false
onmousedown!: (event: Event) => void
cachedSuggestedResults: T[] | null = null
suggestedPromiseLoaded: boolean = false

className(): string {
return '';
Expand All @@ -46,6 +51,10 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
}

setValue(models: T[]) {
if (!this.attrs.onchange) {
return;
}

if (this.attrs.hasOne) {
this.attrs.onchange(models.length ? models[0] : null);
} else {
Expand Down Expand Up @@ -149,19 +158,34 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
}

view() {
const results = this.results(this.debouncedSearchFilter);

const directionUp = this.directionUp();

if (directionUp) {
results?.reverse();
}

return m('.RelationshipSelect', {
className: this.className(),
className: classList(this.className(), {
focused: this.inputIsFocused,
disabled: this.attrs.disabled,
readonly: this.attrs.readonly,
'direction-up': directionUp,
}),
}, [
m('.RelationshipSelect-Form', this.formItems().toArray()),
this.listAvailableModels(this.results(this.debouncedSearchFilter)),
this.listAvailableModels(results),
]);
}

formItems() {
const items = new ItemList();

items.add('input', m('.RelationshipSelect-FakeInput-Wrapper', m('.RelationshipSelect-FakeInput.FormControl', {
className: this.inputIsFocused ? 'focus' : '',
className: classList({
focus: this.inputIsFocused, // deprecated. Check for .focused on .RelationshipSelect
}),
}, this.inputItems().toArray())), 20);

return items;
Expand All @@ -173,6 +197,10 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
items.add('selected', this.normalizedValue().map(model => {
return m('span.RelationshipSelect-Selected', {
onclick: () => {
if (this.attrs.disabled || this.attrs.readonly) {
return;
}

this.toggleModel(model);
this.onready();
},
Expand All @@ -191,12 +219,14 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
this.searchDebouncer = setTimeout(() => {
this.debouncedSearchFilter = this.searchFilter;
this.search(this.debouncedSearchFilter);
}, 300);
}, 300) as any;
},
onkeydown: this.navigator.navigate.bind(this.navigator),
// Use local methods so that other extensions can extend behaviour
onfocus: this.oninputfocus.bind(this),
onblur: this.oninputblur.bind(this),
disabled: this.attrs.disabled,
readonly: this.attrs.readonly,
}), 10);

return items;
Expand Down Expand Up @@ -328,4 +358,61 @@ export default abstract class AbstractRelationshipSelect<T extends Model> extend
onready() {
this.$('input').first().focus().select();
}

/**
* Whether the dropdown should open above the field instead of below
*/
directionUp(): boolean {
if (!(this.element instanceof HTMLElement)) {
return false;
}

const bounding = this.element.getBoundingClientRect();

const spaceAvailableBelow = window.innerHeight - bounding.bottom;

return spaceAvailableBelow < 200;
}

/**
* Shortcut to be used in the results() implementation to render the suggestions.
* If the suggestion is a function, its resulting Promise will be executed and the Select will redraw once results are available.
* If there are zero suggestions, null will be returned to show the spinner identically to a Select without suggestions.
*/
suggestedResults(): T[] | null {
if (!this.attrs.suggest) {
return null;
}

if (typeof this.attrs.suggest === 'function') {
if (!this.suggestedPromiseLoaded) {
this.suggestedPromiseLoaded = true;

this.attrs.suggest().then(results => {
if (Array.isArray(results)) {
if (results.length) {
this.cachedSuggestedResults = results;
}
} else if (results) {
this.cachedSuggestedResults = [results];
}

m.redraw();
});
}

return this.cachedSuggestedResults;
}

if (!Array.isArray(this.attrs.suggest)) {
// No need to check for falsy, it was already done at the beginning
return [this.attrs.suggest];
}

if (this.attrs.suggest.length) {
return this.attrs.suggest;
}

return null;
}
}
4 changes: 2 additions & 2 deletions js/src/common/components/UserRelationshipSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class UserRelationshipSelect extends AbstractRelationshipSelect<U
}

return app.store
.find('users', {
.find<User[]>('users', {
filter: {q: query},
page: {limit: 5},
})
Expand All @@ -31,7 +31,7 @@ export default class UserRelationshipSelect extends AbstractRelationshipSelect<U

results(query: string) {
if (!query) {
return [];
return this.suggestedResults();
}

query = query.toLowerCase();
Expand Down
Loading

0 comments on commit 1cc8bff

Please sign in to comment.