change SelectOrInput to 2 fields

This commit is contained in:
TsXor 2023-10-01 19:37:12 +08:00
parent e94a7b1058
commit f1101a18a2
5 changed files with 59 additions and 152 deletions

2
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,15 @@
import type Mithril from 'mithril';
import type Stream from 'mithril/stream';
import app from 'flarum/admin/app';
import Stream from 'flarum/common/utils/Stream';
import classList from 'flarum/common/utils/classList';
import withAttr from 'flarum/common/utils/withAttr';
import AdminPage, { CommonSettingsItemOptions } from 'flarum/admin/components/AdminPage';
import SelectOrInput from "../../common/Components/SelectOrInput";
import Select from 'flarum/common/components/Select';
import generateElementId from 'flarum/admin/utils/generateElementId';
// forgive me, but this interface is just what its name implies
interface SelectOrInputSettingComponentOptionsWithoutType extends CommonSettingsItemOptions {
options: { [value: string]: Mithril.Children };
checkboxDescription: string;
inputDescription: string;
default: string;
}
@ -16,53 +17,76 @@ export interface SelectOrInputSettingComponentOptions extends SelectOrInputSetti
type: 'select-or-input';
}
interface StreamWithMark<T> extends Stream<T> {
mark?: true;
interface StreamWithUpstream<T> extends Stream<T> {
upstreams?: Array<Stream<unknown>>;
}
function mergeSelectOrInput(
selectStream: Stream<string>, inputStream: Stream<string>,
changed: Array<Stream<unknown>>
) {
return inputStream() || selectStream();
}
export default function (this: AdminPage, _entry: CommonSettingsItemOptions) {
// do this cast because function signature is restricted
const entry = _entry as SelectOrInputSettingComponentOptionsWithoutType;
const [inputId, helpTextId] = [generateElementId(), generateElementId()];
const [selectId, inputId, helpTextId] = [generateElementId(), generateElementId(), generateElementId()];
const { setting, help, label, ...componentAttrs } = entry;
const { default: defaultValue, options, checkboxDescription, ...otherAttrs } = componentAttrs;
const { default: defaultValue, options, inputDescription, className, ...otherAttrs } = componentAttrs;
let settingStream = this.settings[setting] as StreamWithUpstream<string>;
let settingElement: Mithril.Children;
// Trick: add a marker property to the AdminPage's setting stream,
// which contructs and destructs with AdminPage so that default is
// only loaded when the page is created.
// Default should only be loaded once in a page because when checkbox
// is checked, SelectOrInput will set its value to empty string so that
// it becomes an empty input. If default is loaded every time when value
// is falsy, SelectOrInput will not work properly.
const settingStream = this.settings[setting] as StreamWithMark<string>;
let value = settingStream();
if (!settingStream.mark) {
settingStream.mark = true;
if (!value) {
app.data.settings[setting] = defaultValue;
settingStream(defaultValue);
value = settingStream();
if (!settingStream.upstreams) {
let value = settingStream() || defaultValue;
let selectStream = Stream();
let inputStream = Stream();
let upstreams = [selectStream, inputStream]
settingStream = Stream.combine(mergeSelectOrInput, upstreams);
settingStream.upstreams = upstreams;
let useSelect = Object.keys(options).indexOf(value) > -1;
if (useSelect) {
selectStream(value);
inputStream('');
} else {
selectStream(defaultValue);
inputStream(value);
}
this.settings[setting] = settingStream;
}
settingElement = (
<SelectOrInput
id={inputId}
let [selectStream, inputStream] = settingStream.upstreams!;
let selectElement = (
<Select
className={className}
id={selectId}
aria-describedby={helpTextId}
value={value}
value={selectStream()}
options={options}
onchange={settingStream}
checkboxDescription={checkboxDescription}
onchange={selectStream}
{...otherAttrs}
/>
);
let inputElement = (
<input
className={classList('FormControl', className)}
id={inputId}
type='text'
onchange={withAttr('value', inputStream)}
value={inputStream()}
{...otherAttrs}
/>
);
return (
<div className="Form-group">
{label && <label for={inputId}>{label}</label>}
{label && <label for={selectId}>{label}</label>}
<div id={helpTextId} className="helpText">{help}</div>
{settingElement}
{selectElement}
{inputDescription && <label style={{'padding-top': '10px'}} for={inputId}>{inputDescription}</label>}
{inputElement}
</div>
);
}

View File

@ -74,7 +74,7 @@ app.initializers.add('blomstra-search', () => {
help: app.translator.trans('blomstra-search.admin.analyzer.help'),
type: 'select-or-input',
options: Object.fromEntries(languages.entries()),
checkboxDescription: app.translator.trans('blomstra-search.admin.analyzer.custom'),
inputDescription: app.translator.trans('blomstra-search.admin.analyzer.custom'),
default: 'english',
})
.registerSetting({

View File

@ -1,117 +0,0 @@
import Mithril from 'mithril';
import classList from 'flarum/common/utils/classList';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Select from 'flarum/common/components/Select';
import Checkbox, { ICheckboxAttrs } from 'flarum/common/components/Checkbox';
type visibility = 'visible' | 'hidden';
interface ISelectAttrs extends ComponentAttrs {
options: { [k: string]: Mithril.Children };
onchange: (value: string) => void;
value?: string;
disabled?: boolean;
wrapperAttrs?: ComponentAttrs;
}
interface ISubSelectAttrs extends ComponentAttrs {
wrapperAttrs?: ComponentAttrs;
}
export interface ISelectOrInputAttrs<
SelectAttrs extends ISubSelectAttrs = ISubSelectAttrs
> extends ComponentAttrs {
onchange: (value: string) => void;
options: { [k: string]: Mithril.Children };
value?: string;
disabled?: boolean;
checkboxDescription?: string;
selectAttrs?: SelectAttrs;
rawTextInputAttrs?: ComponentAttrs;
}
// when value is `undefined`, choose the first item of select
// when value is in select, use value as value of select
// otherwise, use value as value of input
export default class SelectOrInput<CustomAttrs extends ISelectOrInputAttrs> extends Component<CustomAttrs> {
private onCheckboxChange(checked: boolean, component: Checkbox) {
this.attrs.value = checked ? '' : undefined;
this.attrs.onchange(this.attrs.value!);
m.redraw();
}
private onSelectChange(value: string) {
this.attrs.value = value;
this.attrs.onchange(value);
}
private onInputChange(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.attrs.value = value;
this.attrs.onchange(value);
}
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
const {
class: _class,
className,
onchange,
options,
value: _value,
disabled,
selectAttrs,
rawTextInputAttrs,
...domAttrs
} = this.attrs;
var useinput: boolean; var value: string;
const selectOptionValues = Object.keys(options);
if (typeof _value === 'undefined') {
value = selectOptionValues[0];
this.attrs.value = value;
useinput = false;
} else {
value = _value;
useinput = !(selectOptionValues.indexOf(value!) > -1);
}
const {
class: inputClass,
className: inputClassName,
...otherInputAttrs
} = rawTextInputAttrs ?? {};
return (
<div
className={classList('SelectOrInput', className, _class)}
{...domAttrs}
>
{this.attrs.checkboxDescription ?? ''}
<div style='display: inline-block;'>
<Checkbox
state={useinput}
onchange={this.onCheckboxChange.bind(this)}
/>
</div>
<div style='display: inline-block;'>
{!useinput
? <Select
options={options}
onchange={this.onSelectChange.bind(this)}
disabled={disabled}
value={value}
{...selectAttrs}
/>
: <input
className={classList('FormControl', inputClassName, inputClass)}
type='text'
onchange={this.onInputChange.bind(this)}
disabled={disabled}
value={value}
{...otherInputAttrs}
/>
}
</div>
</div>
);
}
}