feature: implement notify component

This commit is contained in:
Sagi 2022-09-26 23:57:38 +08:00
parent 2bb868f4c8
commit 38d56ff5f4
8 changed files with 415 additions and 349 deletions

View File

@ -6,104 +6,107 @@ import { useClear } from './composition/use-clear';
import { useTextBox } from './composition/use-text-box'; import { useTextBox } from './composition/use-text-box';
export default defineComponent({ export default defineComponent({
name: 'FButtonEdit', name: 'FButtonEdit',
props: buttonEditProps, props: buttonEditProps,
emits: [ emits: [
'updateExtendInfo', 'updateExtendInfo',
'clear', 'clear',
'change', 'change',
'click', 'click',
'clickButton', 'clickButton',
'blur', 'blur',
'focus', 'focus',
'mouseEnterIcon', 'mouseEnterIcon',
'mouseLeaveIcon', 'mouseLeaveIcon',
'keyup', 'keyup',
'keydown', 'keydown',
'inputClick', 'inputClick',
'input', 'input',
'update:modelValue', 'update:modelValue',
], ],
setup(props: ButtonEditProps, context: SetupContext) { setup(props: ButtonEditProps, context: SetupContext) {
const modelValue = ref(props.modelValue); const modelValue = ref(props.modelValue);
const { buttonClass, onClickButton, onMouseEnterButton, onMouseLeaveButton } = useButton(props, context); const { buttonClass, onClickButton, onMouseEnterButton, onMouseLeaveButton } = useButton(props, context);
const displayText = ref(''); const displayText = ref('');
const { const {
hasFocusedTextBox, hasFocusedTextBox,
isTextBoxReadonly, isTextBoxReadonly,
textBoxClass, textBoxClass,
textBoxPlaceholder, textBoxPlaceholder,
textBoxTitle, textBoxTitle,
onBlurTextBox, onBlurTextBox,
onClickTextBox, onClickTextBox,
onFocusTextBox, onFocusTextBox,
onInput, onInput,
onKeyDownTextBox, onKeyDownTextBox,
onKeyUpTextBox, onKeyUpTextBox,
onMouseDownTextBox, onMouseDownTextBox,
onTextBoxValueChange, onTextBoxValueChange,
} = useTextBox(props, context, modelValue, displayText); } = useTextBox(props, context, modelValue, displayText);
const { enableClearButton, showClearButton, onClearValue, onMouseEnterTextBox, onMouseLeaveTextBox } = useClear( const { enableClearButton, showClearButton, onClearValue, onMouseEnterTextBox, onMouseLeaveTextBox } = useClear(
props, props,
context, context,
modelValue, modelValue,
hasFocusedTextBox, hasFocusedTextBox,
displayText displayText
); );
const inputGroupClass = computed(() => ({ const inputGroupClass = computed(() => ({
'input-group': true, 'input-group': true,
'f-state-disable': props.disable, 'f-state-disable': props.disable,
'f-state-editable': props.editable && !props.disable && !props.readonly, 'f-state-editable': props.editable && !props.disable && !props.readonly,
'f-state-readonly': props.readonly && !props.disable, 'f-state-readonly': props.readonly && !props.disable,
'f-state-focus': hasFocusedTextBox, 'f-state-focus': hasFocusedTextBox,
})); }));
return () => { return () => {
return ( return (
<div class="f-cmp-inputgroup" id={props.id}> <div class="f-cmp-inputgroup" id={props.id}>
<div class={[props.customClass, inputGroupClass.value]} onMouseenter={onMouseEnterTextBox} onMouseleave={onMouseLeaveTextBox}> <div
<input class={[props.customClass, inputGroupClass.value]}
name="input-group-value" onMouseenter={onMouseEnterTextBox}
autocomplete={'' + props.autoComplete} onMouseleave={onMouseLeaveTextBox}>
class={textBoxClass.value} <input
disabled={props.disable} name="input-group-value"
maxlength={props.maxLength} autocomplete={'' + props.autoComplete}
minlength={props.minLength} class={textBoxClass.value}
placeholder={textBoxPlaceholder.value} disabled={props.disable}
readonly={isTextBoxReadonly.value} maxlength={props.maxLength}
tabindex={props.tabIndex} minlength={props.minLength}
title={textBoxTitle.value} placeholder={textBoxPlaceholder.value}
type={props.inputType} readonly={isTextBoxReadonly.value}
value={modelValue.value} tabindex={props.tabIndex}
onBlur={onBlurTextBox} title={textBoxTitle.value}
onChange={onTextBoxValueChange} type={props.inputType}
onClick={onClickTextBox} value={modelValue.value}
onFocus={onFocusTextBox} onBlur={onBlurTextBox}
onInput={onInput} onChange={onTextBoxValueChange}
onKeydown={onKeyDownTextBox} onClick={onClickTextBox}
onKeyup={onKeyUpTextBox} onFocus={onFocusTextBox}
onMousedown={onMouseDownTextBox} onInput={onInput}
/> onKeydown={onKeyDownTextBox}
<div class={buttonClass.value}> onKeyup={onKeyUpTextBox}
{enableClearButton.value && ( onMousedown={onMouseDownTextBox}
<span class="input-group-text input-group-clear" v-show={showClearButton.value} onClick={onClearValue}> />
<i class="f-icon modal_close"></i> <div class={buttonClass.value}>
</span> {enableClearButton.value && (
)} <span class="input-group-text input-group-clear" v-show={showClearButton.value} onClick={onClearValue}>
{props.buttonContent && ( <i class="f-icon modal_close"></i>
<span </span>
class="input-group-text input-group-append-button" )}
onClick={onClickButton} {props.buttonContent && (
onMouseenter={onMouseEnterButton} <span
onMouseleave={onMouseLeaveButton} class="input-group-text input-group-append-button"
v-html={props.buttonContent}></span> onClick={onClickButton}
)} onMouseenter={onMouseEnterButton}
</div> onMouseleave={onMouseLeaveButton}
</div> v-html={props.buttonContent}></span>
</div> )}
); </div>
}; </div>
}, </div>
);
};
},
}); });

View File

@ -3,79 +3,79 @@ import { ExtractPropTypes, PropType } from 'vue';
type TextAlignment = 'left' | 'center' | 'right'; type TextAlignment = 'left' | 'center' | 'right';
export const buttonEditProps = { export const buttonEditProps = {
/** /**
* *
*/ */
id: String, id: String,
/** /**
* html标签 * html标签
*/ */
buttonContent: { type: String, default: '<i class="f-icon f-icon-lookup"></i>' }, buttonContent: { type: String, default: '<i class="f-icon f-icon-lookup"></i>' },
/** /**
* *
*/ */
autoComplete: { type: Boolean, default: false }, autoComplete: { type: Boolean, default: false },
/** /**
* *
*/ */
customClass: { type: String, default: '' }, customClass: { type: String, default: '' },
/** /**
* *
*/ */
disable: { type: Boolean, default: false }, disable: { type: Boolean, default: false },
/** /**
* *
*/ */
editable: { type: Boolean, default: true }, editable: { type: Boolean, default: true },
/** /**
* *
*/ */
enableClear: { type: Boolean, default: false }, enableClear: { type: Boolean, default: false },
/** /**
* *
*/ */
modelValue:{type:String,default:''}, modelValue: { type: String, default: '' },
/** /**
* *
*/ */
readonly: { type: Boolean, default: false }, readonly: { type: Boolean, default: false },
/** /**
* *
*/ */
textAlign: { type: String as PropType<TextAlignment>, default: 'left' }, textAlign: { type: String as PropType<TextAlignment>, default: 'left' },
/** /**
* *
*/ */
showButtonWhenDisabled: { type: Boolean, default: false }, showButtonWhenDisabled: { type: Boolean, default: false },
/** /**
* *
*/ */
enableTitle: { type: Boolean, default: false }, enableTitle: { type: Boolean, default: false },
/** /**
* *
*/ */
inputType: { type: String, default: 'text' }, inputType: { type: String, default: 'text' },
/** /**
* *
*/ */
forcePlaceholder: { type: Boolean, default: false }, forcePlaceholder: { type: Boolean, default: false },
/** /**
* *
*/ */
placeholder: { type: String, default: '' }, placeholder: { type: String, default: '' },
/** /**
* *
*/ */
minLength: Number, minLength: Number,
/** /**
* *
*/ */
maxLength: Number, maxLength: Number,
/** /**
* Tab键索引 * Tab键索引
*/ */
tabIndex: Number, tabIndex: Number,
}; };
export type ButtonEditProps = ExtractPropTypes<typeof buttonEditProps>; export type ButtonEditProps = ExtractPropTypes<typeof buttonEditProps>;

View File

@ -3,37 +3,37 @@ import { computed, SetupContext } from 'vue';
import { ButtonEditProps } from '../button-edit.props'; import { ButtonEditProps } from '../button-edit.props';
export function useButton(props: ButtonEditProps, context: SetupContext): UseButton { export function useButton(props: ButtonEditProps, context: SetupContext): UseButton {
const buttonClass = computed(() => ({ const buttonClass = computed(() => ({
'input-group-append': true, 'input-group-append': true,
'append-force-show': props.showButtonWhenDisabled && (props.readonly || props.disable), 'append-force-show': props.showButtonWhenDisabled && (props.readonly || props.disable),
})); }));
const canClickAppendButton = computed(() => props.showButtonWhenDisabled || ((!props.editable || !props.readonly) && !props.disable)); const canClickAppendButton = computed(() => props.showButtonWhenDisabled || ((!props.editable || !props.readonly) && !props.disable));
function onClickButton($event: Event) { function onClickButton($event: Event) {
if (canClickAppendButton.value) { if (canClickAppendButton.value) {
context.emit('clickButton', { origin: $event, value: props.modelValue }); context.emit('clickButton', { origin: $event, value: props.modelValue });
}
$event.stopPropagation();
} }
$event.stopPropagation();
}
function onMouseEnterButton($event: MouseEvent) { function onMouseEnterButton($event: MouseEvent) {
context.emit('mouseEnterIcon', $event); context.emit('mouseEnterIcon', $event);
} }
function onMouseLeaveButton($event: MouseEvent) { function onMouseLeaveButton($event: MouseEvent) {
context.emit('mouseLeaveIcon', $event); context.emit('mouseLeaveIcon', $event);
} }
function onMouseOverButton() { function onMouseOverButton() {
context.emit('mouseOverButton'); context.emit('mouseOverButton');
} }
return { return {
buttonClass, buttonClass,
onClickButton, onClickButton,
onMouseEnterButton, onMouseEnterButton,
onMouseLeaveButton, onMouseLeaveButton,
onMouseOverButton, onMouseOverButton,
}; };
} }

View File

@ -4,64 +4,64 @@ import { UseClear } from './types';
import { useTextBox } from './use-text-box'; import { useTextBox } from './use-text-box';
export function useClear( export function useClear(
props: ButtonEditProps, props: ButtonEditProps,
context: SetupContext, context: SetupContext,
modelValue: Ref<string>, modelValue: Ref<string>,
hasFocusedTextBox: ComputedRef<boolean>, hasFocusedTextBox: ComputedRef<boolean>,
displayText: Ref<string> displayText: Ref<string>
): UseClear { ): UseClear {
const showClearButton = ref(false); const showClearButton = ref(false);
const enableClearButton = computed(() => props.enableClear && !props.readonly && !props.disable); const enableClearButton = computed(() => props.enableClear && !props.readonly && !props.disable);
const { changeTextBoxValue } = useTextBox(props, context, modelValue, displayText); const { changeTextBoxValue } = useTextBox(props, context, modelValue, displayText);
function toggleClearIcon(isShow: boolean) { function toggleClearIcon(isShow: boolean) {
showClearButton.value = isShow; showClearButton.value = isShow;
} }
watch(displayText, () => { watch(displayText, () => {
if (hasFocusedTextBox.value) { if (hasFocusedTextBox.value) {
toggleClearIcon(!!displayText.value); toggleClearIcon(!!displayText.value);
} else { } else {
toggleClearIcon(false); toggleClearIcon(false);
} }
}); });
function onClearValue($event: Event) { function onClearValue($event: Event) {
const flag1 = !props.readonly && !props.disable && props.editable; const flag1 = !props.readonly && !props.disable && props.editable;
const flag2 = !props.editable; const flag2 = !props.editable;
$event.stopPropagation(); $event.stopPropagation();
if (flag1 || flag2) { if (flag1 || flag2) {
changeTextBoxValue('', false); changeTextBoxValue('', false);
toggleClearIcon(!showClearButton.value); toggleClearIcon(!showClearButton.value);
context.emit('clear'); context.emit('clear');
}
} }
}
function onMouseEnterTextBox($event: Event) { function onMouseEnterTextBox($event: Event) {
if (!enableClearButton.value) { if (!enableClearButton.value) {
return; return;
}
if (!modelValue.value) {
toggleClearIcon(false);
return;
}
if ((!props.editable || !props.readonly) && !props.disable) {
toggleClearIcon(true);
}
} }
if (!modelValue.value) {
toggleClearIcon(false);
return;
}
if ((!props.editable || !props.readonly) && !props.disable) {
toggleClearIcon(true);
}
}
function onMouseLeaveTextBox($event: Event) { function onMouseLeaveTextBox($event: Event) {
if (!enableClearButton.value) { if (!enableClearButton.value) {
return; return;
}
toggleClearIcon(false);
} }
toggleClearIcon(false);
}
return { return {
enableClearButton, enableClearButton,
showClearButton, showClearButton,
onClearValue, onClearValue,
onMouseEnterTextBox, onMouseEnterTextBox,
onMouseLeaveTextBox, onMouseLeaveTextBox,
}; };
} }

View File

@ -1,59 +1,118 @@
import { computed, defineComponent, SetupContext } from 'vue'; import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
import { NotifyData } from '../notify.props'; import { NotifyButton, NotifyData } from '../notify.props';
import { ToastProps, toastProps } from './toast.props'; import { ToastProps, toastProps } from './toast.props';
export default defineComponent({ export default defineComponent({
name: 'Toast', name: 'Toast',
props: toastProps, props: toastProps,
emits: [], emits: ['close', 'click'],
setup: (props: ToastProps, context: SetupContext) => { setup: (props: ToastProps, context: SetupContext) => {
const toastClass = computed(() => ({ const animateIn = ref(props.animate);
toast: true, const animateEnd = 'fadeOut';
})); const toast = computed(() => {
return props.options as NotifyData;
});
const showingToast = ref(false);
const toast = computed(() => { const toastClass = computed(() => {
return {} as NotifyData; const classObject = {
}); animated: showingToast.value,
toast: true,
};
classObject[props.animate] = false;
classObject[animateEnd] = showingToast.value;
classObject[toast.value.type] = true;
if (toast.value.theme) {
classObject[toast.value.theme] = true;
}
return classObject;
});
const toastIconClass = computed(() => ({ const toastIconClass = computed(() => {
'f-icon': true, const hasSpecialToastType = toast.value && toast.value.type;
})); const iconType = hasSpecialToastType ? toast.value.type.replace('toasty-type-', '') : 'default';
const iconTypeName = `f-icon-${iconType}`;
const classObject = { 'f-icon': true };
classObject[iconTypeName] = true;
return classObject;
});
const shouldShowTips = computed(() => toast.value.title || toast.value.msg); const shouldShowTips = computed(() => toast.value.title || toast.value.msg);
const shouldShowTitle = computed(() => toast.value.title && toast.value.msg); const shouldShowTitle = computed(() => toast.value.title && toast.value.msg);
const shouldShowCloseButton = computed(() => { const shouldShowCloseButton = computed(() => {
return true; return true;
}); });
function onCloseToast($event: Event) {} const shouldShowButtonsInTitle = computed(() => !!toast.value.buttons || !!context.slots.default);
return () => { function onCloseToast($event: Event) {
return ( $event.stopPropagation();
<div class={toastClass}> $event.preventDefault();
{shouldShowCloseButton.value && ( showingToast.value = false;
<button class="toast-close f-btn-icon f-bare" onClick={onCloseToast}> setTimeout(() => {
<span class="f-icon modal_close"></span> context.emit('close', toast.value);
</button> }, 200);
)} }
{shouldShowTips.value && (
<section class="modal-tips"> function onClickButton($event: Event, notifyButton: NotifyButton) {}
<div class="float-left modal-tips-iconwrap">
<span class={toastIconClass}></span> function getNotifyButtonClass(notifyButton: NotifyButton) {
</div> return `f-preten-link ${notifyButton.customClass ? notifyButton.customClass : ''}`;
<div class="modal-tips-content"> }
{shouldShowTitle.value && (
<> watch(animateIn, () => {
<h5 class="toast-title modal-tips-title" v-html={toast.value.title}></h5> const animateInClass = animateIn.value || 'bounceInRight';
<p class="toast-msg" v-html={toast.value.msg}></p> const animateOutClass = 'fadeOut';
</> });
)}
</div> const renderNotifyButtons = () => {
</section> return (
)} <>
</div> <div class="after-toast-msg text-right">
); {!context.slots.default &&
}; toast.value.buttons?.map((notifyButton: NotifyButton) => {
}, return (
<span
class={getNotifyButtonClass(notifyButton)}
onClick={($event) => onClickButton($event, notifyButton)}>
{notifyButton.text}
</span>
);
})}
{context.slots.default && context.slots.default()}
</div>
</>
);
};
return () => {
return (
<div class={toastClass}>
{shouldShowCloseButton.value && (
<button class="toast-close f-btn-icon f-bare" onClick={onCloseToast}>
<span class="f-icon modal_close"></span>
</button>
)}
{shouldShowTips.value && (
<section class="modal-tips">
<div class="float-left modal-tips-iconwrap">
<span class={toastIconClass}></span>
</div>
<div class="modal-tips-content">
{shouldShowTitle.value && (
<>
<h5 class="toast-title modal-tips-title" v-html={toast.value.title}></h5>
<p class="toast-msg" v-html={toast.value.msg}></p>
{shouldShowButtonsInTitle.value && renderNotifyButtons()}
</>
)}
</div>
</section>
)}
</div>
);
};
},
}); });

View File

@ -1,8 +1,9 @@
import { ExtractPropTypes, PropType } from 'vue'; import { ExtractPropTypes, PropType } from 'vue';
import { ToastyAnimate } from '../notify.props'; import { NotifyData, ToastyAnimate } from '../notify.props';
export const toastProps = { export const toastProps = {
animate: { type: String as PropType<ToastyAnimate>, default: 'fadeIn' }, animate: { type: String as PropType<ToastyAnimate>, default: 'fadeIn' },
options: { type: Object as PropType<NotifyData> },
}; };
export type ToastProps = ExtractPropTypes<typeof toastProps>; export type ToastProps = ExtractPropTypes<typeof toastProps>;

View File

@ -1,35 +1,37 @@
import { computed, defineComponent, SetupContext } from 'vue'; import { computed, defineComponent, ref, SetupContext } from 'vue';
import { NotifyContainerComponent } from './notify-container.component'; import { NotifyContainerComponent } from './notify-container.component';
import { NotifyData, NotifyProps, notifyProps } from './notify.props'; import { NotifyData, NotifyProps, notifyProps } from './notify.props';
export default defineComponent({ export default defineComponent({
name: 'Notify', name: 'Notify',
props: notifyProps, props: notifyProps,
emits: [], emits: [],
setup(props: NotifyProps, context: SetupContext) { setup(props: NotifyProps, context: SetupContext) {
const notifyClass = computed(() => ({ const notifyClass = computed(() => ({
'farris-notify': true, 'farris-notify': true,
})); }));
const notifyStyle = computed(() => ({ const notifyStyle = computed(() => ({
left: '', left: '',
right: '', right: '',
top: '', top: '',
bottom: '', bottom: '',
})); }));
const toasts = computed(() => { const toasts = computed(() => {
return props.toasts ? props.toasts : []; return props.toasts ? props.toasts : [];
}); });
return () => {
return (
<div id={props.id} class={notifyClass.value} style={notifyStyle.value}> return () => {
{toasts.value.map((toastData: NotifyData) => { return (
return <f-toast v-model={toastData} animate={props.animate} onClose={} onClick={}></f-toast>; <div id={props.id} class={notifyClass.value} style={notifyStyle.value}>
})} {toasts.value.map((toastData: NotifyData) => {
</div> return <f-toast v-model={toastData} animate={props.animate} onClose={} onClick={}></f-toast>;
); })}
}; </div>
}, );
};
},
}); });

View File

@ -1,44 +1,45 @@
import { NotifyData } from './notify.props';
import { ExtractPropTypes, PropType } from 'vue'; import { ExtractPropTypes, PropType } from 'vue';
export type NotifyPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'top-center' | 'bottom-center' | 'center-center'; export type NotifyPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'top-center' | 'bottom-center' | 'center-center';
export type ToastyAnimate = export type ToastyAnimate =
| 'bounceInRight' | 'bounceInRight'
| 'bounceInLeft' | 'bounceInLeft'
| 'bounceInRight' | 'bounceInRight'
| 'bounceInLeft' | 'bounceInLeft'
| 'bounceInDown' | 'bounceInDown'
| 'bounceInUp' | 'bounceInUp'
| 'bounceIn' | 'bounceIn'
| 'fadeIn'; | 'fadeIn';
export interface NotifyButton { export interface NotifyButton {
customClass?: string; customClass?: string;
text: string; text: string;
disable?: boolean; disable?: boolean;
onClick?: ($event: Event, component: any) => any; onClick?: ($event: Event, component: any) => any;
} }
export interface NotifyData { export interface NotifyData {
type: string; type: string;
title?: string; title?: string;
msg?: string; msg?: string;
/** 按钮列表模板 */ /** 按钮列表模板 */
buttons?: Array<NotifyButton>; buttons?: Array<NotifyButton>;
showClose?: boolean; showClose?: boolean;
theme?: string; theme?: string;
timeout?: number; timeout?: number;
onAdd?: () => void; onAdd?: () => void;
onRemove?: () => void; onRemove?: () => void;
id?: number | string; id?: number | string;
} }
// export interface NotifyData extends NotifyOptions {} // export interface NotifyData extends NotifyOptions {}
export const notifyProps = { export const notifyProps = {
id: { type: String }, id: { type: String },
animate: { type: String as PropType<ToastyAnimate>, default: 'fadeIn' }, animate: { type: String as PropType<ToastyAnimate>, default: 'fadeIn' },
position: { type: String as PropType<NotifyPosition>, default: 'toasty-position-top-center' }, position: { type: String as PropType<NotifyPosition>, default: 'toasty-position-top-center' },
toasts: { type: Array<NotifyData> }, toasts: { type: Array<NotifyData> },
}; };
export type NotifyProps = ExtractPropTypes<typeof notifyProps>; export type NotifyProps = ExtractPropTypes<typeof notifyProps>;