opal-components
Version:
[Rionite](https://github.com/Riim/Rionite) component set.
830 lines (676 loc) • 18.7 kB
text/typescript
import { getText } from '@riim/gettext';
import { nextTick } from '@riim/next-tick';
import {
Cell,
define,
IEvent,
ObservableList
} from 'cellx';
import { computed, observable } from 'cellx-decorators';
import {
Component,
IComponentElement,
IDisposableListening,
RtIfThen,
RtRepeat
} from 'rionite';
import { isFocusable } from '../../utils/isFocusable';
import { OpalButton } from '../opal-button';
import { OpalDropdown } from '../opal-dropdown';
import { OpalFilteredList } from '../opal-filtered-list';
import { OpalLoadedList } from '../opal-loaded-list';
import { OpalTextInput } from '../opal-text-input';
import './index.css';
import { isEqualArray } from './isEqualArray';
import { OpalSelectOption } from './opal-select-option';
import template from './template.nelm';
export { OpalSelectOption };
let map = Array.prototype.map;
export interface IDataListItem {
[name: string]: any;
}
export type TDataList = ObservableList<IDataListItem>;
export type TViewModel = ObservableList<IDataListItem>;
let defaultDataListItemSchema = Object.freeze({
value: 'value',
text: 'text',
disabled: 'disabled'
});
let defaultVMItemSchema = Object.freeze({ value: 'value', text: 'text', disabled: 'disabled' });
.Config<OpalSelect>({
elementIs: 'OpalSelect',
params: {
viewType: String,
size: 'm',
multiple: { default: false, readonly: true },
dataList: { type: Object },
dataListKeypath: { type: String, readonly: true },
dataListItemSchema: { type: eval, default: defaultDataListItemSchema, readonly: true },
value: eval,
viewModel: { type: Object },
viewModelItemSchema: { type: eval, default: defaultVMItemSchema, readonly: true },
addNewItem: { type: Object, readonly: true },
text: String,
maxTextLength: 20,
placeholder: getText.t('Не выбрано'),
tabIndex: 0,
focused: false,
disabled: false
},
template,
events: {
'menu-slot': {
loaded(evt) {
if (this._onсeFocusedAfterLoading || evt.target !== this.$('loaded-list')) {
return;
}
this._onсeFocusedAfterLoading = true;
this._focusOptions();
let focusTarget = this.$<HTMLElement | OpalTextInput>('focus');
if (!focusTarget) {
let filteredList = this.$<OpalFilteredList>('filtered-list');
if (filteredList) {
focusTarget = filteredList.$<OpalTextInput>('query-input');
}
}
if (focusTarget) {
nextTick(() => {
focusTarget!.focus();
});
}
}
}
}
})
export class OpalSelect extends Component {
static defaultDataListItemSchema = defaultDataListItemSchema;
static defaultViewModelItemSchema = defaultVMItemSchema;
dataList: TDataList | null;
_dataListItemValueFieldName: string;
_dataListItemTextFieldName: string;
_dataListItemDisabledFieldName: string;
viewModel: TViewModel;
_viewModelItemValueFieldName: string;
_viewModelItemTextFieldName: string;
_viewModelItemDisabledFieldName: string;
get value(): Array<string> {
return this.viewModel.map(item => item[this._viewModelItemValueFieldName]);
}
_addNewItem: ((text: string) => Promise<{ [name: string]: string }>) | null;
get _buttonText(): string {
let text = this.viewModel
.map((item): string => item[this._viewModelItemTextFieldName])
.join(', ');
if (!text) {
return this.params.placeholder;
}
if (text.length > this.params.maxTextLength) {
text = getText.nt('Выбран{n:|о|о} {n}', this.viewModel.length);
}
return text;
}
optionElements: NodeListOf<IComponentElement>;
optionsCell: Cell<Array<OpalSelectOption>>;
get options(): Array<OpalSelectOption> {
return map.call(this.optionElements, (option: IComponentElement) => option.$component);
}
_needOptionsUpdating = false;
_notUpdateOptions = false;
_opened: boolean = false;
_valueOnOpen: Array<string>;
_onсeFocusedAfterLoading: boolean = false;
_isParamDataListSpecified: boolean;
_documentFocusListening: IDisposableListening;
_documentKeyDownListening: IDisposableListening | null | undefined;
initialize() {
let params = this.params;
if (params.dataListKeypath) {
define(
this,
'dataList',
new Cell(Function(`return this.${params.dataListKeypath};`), {
context: this.ownerComponent || window
})
);
this._isParamDataListSpecified = true;
} else if (params.$specified.has('dataList')) {
define(this, 'dataList', () => params.dataList);
this._isParamDataListSpecified = true;
} else {
this.dataList = null;
this._isParamDataListSpecified = false;
}
let dataListItemSchema = params.dataListItemSchema;
let defaultDataListItemSchema = (this.constructor as typeof OpalSelect)
.defaultDataListItemSchema;
this._dataListItemValueFieldName =
dataListItemSchema.value || defaultDataListItemSchema.value;
this._dataListItemTextFieldName = dataListItemSchema.text || defaultDataListItemSchema.text;
this._dataListItemDisabledFieldName =
dataListItemSchema.disabled || defaultDataListItemSchema.disabled;
this.viewModel = params.viewModel || new ObservableList();
let vmItemSchema = params.viewModelItemSchema;
let defaultVMItemSchema = (this.constructor as typeof OpalSelect)
.defaultViewModelItemSchema;
this._viewModelItemValueFieldName = vmItemSchema.value || defaultVMItemSchema.value;
this._viewModelItemTextFieldName = vmItemSchema.text || defaultVMItemSchema.text;
this._viewModelItemDisabledFieldName =
vmItemSchema.disabled || defaultVMItemSchema.disabled;
this._addNewItem = params.addNewItem;
}
ready() {
this.optionElements = this.element.getElementsByClassName('OpalSelectOption') as NodeListOf<
IComponentElement
>;
if (this.params.viewModel && !this.params.value) {
this._needOptionsUpdating = true;
} else {
this._notUpdateOptions = true;
this.$<OpalDropdown>('menu')!.renderContent();
this._notUpdateOptions = false;
this._initViewModel();
}
}
_initViewModel() {
let value = this.params.value;
let selectedOptions: Array<OpalSelectOption> | undefined;
if (value) {
if (!Array.isArray(value)) {
throw new TypeError('Parameter "value" must be an array');
}
this._notUpdateOptions = true;
this.viewModel.clear();
if (value.length) {
if (this.params.multiple) {
selectedOptions = this.options.filter(
option => value.indexOf(option.value) != -1
);
} else {
value = value[0];
let selectedOption = this.options.find(option => option.value == value);
if (selectedOption) {
selectedOptions = [selectedOption];
}
}
}
} else if (this.params.multiple) {
selectedOptions = this.options.filter(option => option.selected);
} else {
let selectedOption = this.options.find(option => option.selected);
if (selectedOption) {
selectedOptions = [selectedOption];
}
}
if (selectedOptions && selectedOptions.length) {
this._notUpdateOptions = true;
this.viewModel.addRange(
selectedOptions.map(option => ({
[this._viewModelItemValueFieldName]: option.value,
[this._viewModelItemTextFieldName]: option.text
}))
);
}
this._notUpdateOptions = false;
if (value) {
this._updateOptions();
}
}
elementAttached() {
if (this.params.focused) {
this._documentKeyDownListening = this.listenTo(
document,
'keydown',
this._onDocumentKeyDown
);
this.focus();
}
this.listenTo(this, {
'param-value-change': this._onParamValueChange,
'param-view-model-change': this._onParamViewModelChange,
'param-focused-change': this._onParamFocusedChange
});
this.listenTo(this.viewModel, 'change', this._onViewModelChange);
this.listenTo('button', {
focus: this._onButtonFocus,
blur: this._onButtonBlur,
click: this._onButtonClick
});
this.listenTo('menu', {
'param-opened-change': this._onMenuParamOpenedChange,
'<OpalSelectOption>select': this._onMenuSelectOptionSelect,
'<OpalSelectOption>deselect': this._onMenuSelectOptionDeselect,
'<OpalTextInput>confirm': this._onMenuTextInputConfirm,
'<*>change': this._onMenuChange
});
}
_onParamValueChange(evt: IEvent) {
let vm = this.viewModel;
let value: Array<string> | null = evt.data.value;
if (value) {
if (!Array.isArray(value)) {
throw new TypeError('Parameter "value" must be an array');
}
if (value.length) {
let multiple = this.params.multiple;
if (
multiple ||
!vm.length ||
value[0] != vm.get(0)![this._viewModelItemValueFieldName]
) {
if (this._needOptionsUpdating) {
this._needOptionsUpdating = false;
this._notUpdateOptions = true;
this.$<OpalDropdown>('menu')!.renderContent();
this._notUpdateOptions = false;
this._updateViewModel(value, multiple);
} else {
this._updateViewModel(value, multiple);
}
}
return;
}
}
vm.clear();
}
_updateViewModel(value: any, multiple: boolean) {
let vm = this.viewModel;
let vmItemValueFieldName = this._viewModelItemValueFieldName;
let vmItemTextFieldName = this._viewModelItemTextFieldName;
if (multiple) {
this.options.forEach(option => {
let optionValue = option.value;
let itemIndex = vm.findIndex(item => item[vmItemValueFieldName] == optionValue);
if (value.indexOf(optionValue) == -1) {
if (itemIndex != -1) {
vm.removeAt(itemIndex);
}
} else if (itemIndex == -1) {
vm.add({
[vmItemValueFieldName]: optionValue,
[vmItemTextFieldName]: option.text
});
}
});
} else {
value = value[0];
if (
!this.options.some(option => {
if (option.value != value) {
return false;
}
vm.set(0, {
[vmItemValueFieldName]: value,
[vmItemTextFieldName]: option.text
});
return true;
})
) {
vm.clear();
}
}
}
_onParamViewModelChange(evt: IEvent) {
if (evt.data.value != this.viewModel) {
throw new TypeError('Parameter "viewModel" is readonly');
}
}
_onParamFocusedChange(evt: IEvent) {
if (evt.data.value) {
if (!this._documentKeyDownListening) {
this._documentKeyDownListening = this.listenTo(
document,
'keydown',
this._onDocumentKeyDown
);
}
this.focus();
this.emit('focus');
} else {
if (!this._opened) {
this._documentKeyDownListening!.stop();
this._documentKeyDownListening = null;
}
this.blur();
this.emit('blur');
}
}
_onViewModelChange() {
if (!this._needOptionsUpdating && !this._notUpdateOptions) {
this._updateOptions();
}
}
_onButtonFocus() {
this.params.focused = true;
}
_onButtonBlur() {
this.params.focused = false;
}
_onButtonClick(evt: IEvent<OpalButton>) {
if (evt.target.checked) {
this.open();
} else {
this.close();
}
}
_onMenuParamOpenedChange(evt: IEvent): false {
if (!evt.data.value) {
this.close();
}
return false;
}
_onMenuSelectOptionSelect(evt: IEvent<OpalSelectOption>): false {
let vm = this.viewModel;
let vmItem = {
[this._viewModelItemValueFieldName]: evt.target.value,
[this._viewModelItemTextFieldName]: evt.target.text
};
if (this.params.multiple) {
this._notUpdateOptions = true;
vm.add(vmItem);
this._notUpdateOptions = false;
this.emit('select');
} else {
if (vm.length) {
vm.set(0, vmItem);
} else {
vm.add(vmItem);
}
this.close();
this.focus();
this.emit('select');
this.emit('change');
}
return false;
}
_onMenuSelectOptionDeselect(evt: IEvent<OpalSelectOption>): false {
if (this.params.multiple) {
let value = evt.target.value;
this._notUpdateOptions = true;
this.viewModel.removeAt(this.viewModel.findIndex(item => item.value == value));
this._notUpdateOptions = false;
} else {
evt.target.select();
this.close();
this.focus();
}
this.emit('deselect');
return false;
}
_onMenuTextInputConfirm(evt: IEvent<OpalTextInput>): false | void {
let textInput = evt.target;
if (textInput !== this.$('new-item-input')) {
return;
}
if (!this._addNewItem) {
throw new TypeError('Parameter "addNewItem" is required');
}
let text = textInput.value!;
textInput.clear();
textInput.params.loading = true;
textInput.params.disabled = true;
this._addNewItem(text).then(
(newItem: { [name: string]: string }) => {
textInput.params.loading = false;
textInput.params.disabled = false;
let value = newItem[this._viewModelItemValueFieldName];
let text = newItem[this._viewModelItemTextFieldName];
if (this.dataList) {
this.dataList.add({
[this._dataListItemValueFieldName]: value,
[this._dataListItemTextFieldName]: text
});
}
let loadedList = this.$<OpalLoadedList>('loaded-list');
if (loadedList) {
loadedList.params.query = '';
}
let vm = this.viewModel;
let vmItem = {
[this._viewModelItemValueFieldName]: value,
[this._viewModelItemTextFieldName]: text
};
if (this.params.multiple) {
vm.add(vmItem);
this.emit('input');
} else {
if (vm.length) {
vm.set(0, vmItem);
} else {
vm.add(vmItem);
}
this.close();
this.focus();
this.emit('input');
this.emit('change');
}
},
() => {
textInput.params.loading = false;
textInput.params.disabled = false;
}
);
return false;
}
_onMenuChange(evt: IEvent) {
if (
!this._notUpdateOptions &&
(evt.target instanceof RtIfThen || evt.target instanceof RtRepeat)
) {
this.optionsCell.pull();
this._updateOptions();
}
}
open(): boolean {
if (this._opened) {
return false;
}
this._opened = true;
this._valueOnOpen = this.viewModel.map(
(item): string => item[this._viewModelItemValueFieldName]
);
this._documentFocusListening = this.listenTo(
document,
'focus',
this._onDocumentFocus,
this,
true
);
if (!this._documentKeyDownListening) {
this._documentKeyDownListening = this.listenTo(
document,
'keydown',
this._onDocumentKeyDown
);
}
this.$<OpalButton>('button')!.check();
this._notUpdateOptions = true;
this.$<OpalDropdown>('menu')!.open();
this._notUpdateOptions = false;
if (this._needOptionsUpdating) {
this._needOptionsUpdating = false;
this._updateOptions();
}
let loadedList = this.$<OpalLoadedList>('loaded-list');
if (loadedList) {
loadedList!.checkLoading();
}
let focusTarget = this.$<HTMLElement | OpalTextInput>('focus');
if (focusTarget) {
focusTarget.focus();
} else {
let filteredList = this.$<OpalFilteredList>('filtered-list');
if (filteredList) {
focusTarget = filteredList.$<OpalTextInput>('query-input');
}
if (focusTarget) {
focusTarget.focus();
} else {
this._focusOptions();
}
}
return true;
}
close(): boolean {
if (!this._opened) {
return false;
}
this._opened = false;
this._documentFocusListening.stop();
if (!this.params.focused) {
this._documentKeyDownListening!.stop();
this._documentKeyDownListening = null;
}
this.$<OpalButton>('button')!.uncheck();
this.$<OpalDropdown>('menu')!.close();
if (this.params.multiple) {
if (
!isEqualArray(
this.viewModel.map((item): string => item[this._viewModelItemValueFieldName]),
this._valueOnOpen
)
) {
this.emit('change');
}
}
return true;
}
toggle(value?: boolean): boolean {
if (value !== undefined) {
return value ? this.open() : !this.close();
}
return this.open() || !this.close();
}
_onDocumentFocus(evt: Event) {
if (!isFocusable(evt.target as HTMLElement)) {
return;
}
if (!this.element.contains((evt.target as Node).parentNode!)) {
this.close();
}
}
_onDocumentKeyDown(evt: KeyboardEvent) {
switch (evt.which) {
case 32 /* Space */: {
if (this._opened) {
if (this.params.focused) {
evt.preventDefault();
this.close();
}
} else {
evt.preventDefault();
this.open();
}
break;
}
case 38 /* Up */: {
evt.preventDefault();
if (this._opened) {
if (document.activeElement == document.body) {
if (this._focusOptions()) {
document.body.classList.remove('_no-focus-highlight');
}
} else {
let options = this.options;
for (let i = 1, l = options.length; i < l; i++) {
if (options[i].params.focused) {
do {
let option = options[--i];
if (!option.params.disabled && option.element.offsetWidth) {
document.body.classList.remove('_no-focus-highlight');
option.focus();
break;
}
} while (i);
break;
}
}
}
} else {
this.open();
}
break;
}
case 40 /* Down */: {
evt.preventDefault();
if (this._opened) {
if (document.activeElement == document.body) {
if (this._focusOptions()) {
document.body.classList.remove('_no-focus-highlight');
}
} else {
let options = this.options;
for (let i = 0, l = options.length - 1; i < l; i++) {
if (options[i].params.focused) {
do {
let option = options[++i];
if (!option.params.disabled && option.element.offsetWidth) {
document.body.classList.remove('_no-focus-highlight');
option.focus();
break;
}
} while (i < l);
break;
}
}
}
} else {
this.open();
}
break;
}
case 27 /* Esc */: {
evt.preventDefault();
this.close();
this.focus();
break;
}
}
}
_updateOptions() {
let vm = this.viewModel;
let vmItemValueFieldName = this._viewModelItemValueFieldName;
let vmItemDisabledFieldName = this._viewModelItemDisabledFieldName;
this.options.forEach(option => {
let value = option.value;
let item = vm.find(item => item[vmItemValueFieldName] == value);
if (item) {
option.selected = true;
option.disabled = item[vmItemDisabledFieldName];
} else {
option.selected = false;
}
});
}
_focusOptions(): boolean {
let options = this.options;
let optionForFocus;
for (let i = 0, l = options.length; i < l; i++) {
let option = options[i];
if (!option.params.disabled) {
if (option.selected) {
optionForFocus = option;
break;
}
if (!optionForFocus) {
optionForFocus = option;
}
}
}
if (optionForFocus) {
optionForFocus.focus();
return true;
}
return false;
}
focus(): OpalSelect {
this.$<OpalSelect>('button')!.focus();
return this;
}
blur(): OpalSelect {
this.$<OpalSelect>('button')!.blur();
return this;
}
}