chore: move mixins

This commit is contained in:
chenjiahan 2020-07-05 16:00:10 +08:00
parent 3a2e20eb52
commit 4a25ebf9ab
14 changed files with 844 additions and 0 deletions

View File

@ -0,0 +1,33 @@
/**
* Bind event when mounted or activated
*/
import { on, off } from '../utils/dom/event';
type BindEventMixinThis = {
binded: boolean;
};
type BindEventHandler = (bind: Function, isBind: boolean) => void;
export function BindEventMixin(handler: BindEventHandler) {
function bind(this: BindEventMixinThis) {
if (!this.binded) {
handler.call(this, on, true);
this.binded = true;
}
}
function unbind(this: BindEventMixinThis) {
if (this.binded) {
handler.call(this, off, false);
this.binded = false;
}
}
return {
mounted: bind,
activated: bind,
deactivated: unbind,
beforeDestroy: unbind,
};
}

148
src-next/mixins/checkbox.js Normal file
View File

@ -0,0 +1,148 @@
/**
* Common part of Checkbox & Radio
*/
import Icon from '../icon';
import { FieldMixin } from './field';
import { ChildrenMixin } from './relation';
import { addUnit } from '../utils';
export const CheckboxMixin = ({ parent, bem, role }) => ({
mixins: [ChildrenMixin(parent), FieldMixin],
props: {
name: null,
value: null,
disabled: Boolean,
iconSize: [Number, String],
checkedColor: String,
labelPosition: String,
labelDisabled: Boolean,
shape: {
type: String,
default: 'round',
},
bindGroup: {
type: Boolean,
default: true,
},
},
computed: {
disableBindRelation() {
return !this.bindGroup;
},
isDisabled() {
return (this.parent && this.parent.disabled) || this.disabled;
},
direction() {
return (this.parent && this.parent.direction) || null;
},
iconStyle() {
const checkedColor =
this.checkedColor || (this.parent && this.parent.checkedColor);
if (checkedColor && this.checked && !this.isDisabled) {
return {
borderColor: checkedColor,
backgroundColor: checkedColor,
};
}
},
tabindex() {
if (this.isDisabled || (role === 'radio' && !this.checked)) {
return -1;
}
return 0;
},
},
methods: {
onClick(event) {
const { target } = event;
const { icon } = this.$refs;
const iconClicked = icon === target || icon.contains(target);
if (!this.isDisabled && (iconClicked || !this.labelDisabled)) {
this.toggle();
// wait for toggle method to complete
// so we can get the changed value in the click event listener
setTimeout(() => {
this.$emit('click', event);
});
} else {
this.$emit('click', event);
}
},
genIcon() {
const { checked } = this;
const iconSize = this.iconSize || (this.parent && this.parent.iconSize);
return (
<div
ref="icon"
class={bem('icon', [
this.shape,
{ disabled: this.isDisabled, checked },
])}
style={{ fontSize: addUnit(iconSize) }}
>
{this.slots('icon', { checked }) || (
<Icon name="success" style={this.iconStyle} />
)}
</div>
);
},
genLabel() {
const slot = this.slots();
if (slot) {
return (
<span
class={bem('label', [
this.labelPosition,
{ disabled: this.isDisabled },
])}
>
{slot}
</span>
);
}
},
},
render() {
const Children = [this.genIcon()];
if (this.labelPosition === 'left') {
Children.unshift(this.genLabel());
} else {
Children.push(this.genLabel());
}
return (
<div
role={role}
class={bem([
{
disabled: this.isDisabled,
'label-disabled': this.labelDisabled,
},
this.direction,
])}
tabindex={this.tabindex}
aria-checked={String(this.checked)}
onClick={this.onClick}
>
{Children}
</div>
);
},
});

View File

@ -0,0 +1,31 @@
/**
* Listen to click outside event
*/
import { on, off } from '../utils/dom/event';
export const ClickOutsideMixin = (config) => ({
props: {
closeOnClickOutside: {
type: Boolean,
default: true,
},
},
data() {
const clickOutsideHandler = (event) => {
if (this.closeOnClickOutside && !this.$el.contains(event.target)) {
this[config.method]();
}
};
return { clickOutsideHandler };
},
mounted() {
on(document, config.event, this.clickOutsideHandler);
},
beforeDestroy() {
off(document, config.event, this.clickOutsideHandler);
},
});

View File

@ -0,0 +1,44 @@
import { on, off } from '../utils/dom/event';
import { BindEventMixin } from './bind-event';
export const CloseOnPopstateMixin = {
mixins: [
BindEventMixin(function (bind, isBind) {
this.handlePopstate(isBind && this.closeOnPopstate);
}),
],
props: {
closeOnPopstate: Boolean,
},
data() {
return {
bindStatus: false,
};
},
watch: {
closeOnPopstate(val) {
this.handlePopstate(val);
},
},
methods: {
handlePopstate(bind) {
/* istanbul ignore if */
if (this.$isServer) {
return;
}
if (this.bindStatus !== bind) {
this.bindStatus = bind;
const action = bind ? on : off;
action(window, 'popstate', () => {
this.close();
this.shouldReopen = false;
});
}
},
},
};

26
src-next/mixins/field.js Normal file
View File

@ -0,0 +1,26 @@
export const FieldMixin = {
inject: {
vanField: {
default: null,
},
},
watch: {
value() {
const field = this.vanField;
if (field) {
field.resetValidation();
field.validateWithTrigger('onChange');
}
},
},
created() {
const field = this.vanField;
if (field && !field.children) {
field.children = this;
}
},
};

View File

@ -0,0 +1,16 @@
import { OverlayConfig } from './overlay';
export type StackItem = {
vm: any;
overlay: any;
config: OverlayConfig;
};
export const context = {
zIndex: 2000,
lockCount: 0,
stack: [] as StackItem[],
find(vm: any): StackItem | undefined {
return this.stack.filter((item) => item.vm === vm)[0];
},
};

View File

@ -0,0 +1,220 @@
// Context
import { context } from './context';
import {
openOverlay,
closeOverlay,
updateOverlay,
removeOverlay,
} from './overlay';
// Utils
import { on, off, preventDefault } from '../../utils/dom/event';
import { removeNode } from '../../utils/dom/node';
import { getScroller } from '../../utils/dom/scroll';
// Mixins
import { TouchMixin } from '../touch';
import { PortalMixin } from '../portal';
import { CloseOnPopstateMixin } from '../close-on-popstate';
export const popupMixinProps = {
// whether to show popup
value: Boolean,
// whether to show overlay
overlay: Boolean,
// overlay custom style
overlayStyle: Object,
// overlay custom class name
overlayClass: String,
// whether to close popup when click overlay
closeOnClickOverlay: Boolean,
// z-index
zIndex: [Number, String],
// prevent body scroll
lockScroll: {
type: Boolean,
default: true,
},
// whether to lazy render
lazyRender: {
type: Boolean,
default: true,
},
};
export function PopupMixin(options = {}) {
return {
mixins: [
TouchMixin,
CloseOnPopstateMixin,
PortalMixin({
afterPortal() {
if (this.overlay) {
updateOverlay();
}
},
}),
],
props: popupMixinProps,
data() {
return {
inited: this.value,
};
},
computed: {
shouldRender() {
return this.inited || !this.lazyRender;
},
},
watch: {
value(val) {
const type = val ? 'open' : 'close';
this.inited = this.inited || this.value;
this[type]();
if (!options.skipToggleEvent) {
this.$emit(type);
}
},
overlay: 'renderOverlay',
},
mounted() {
if (this.value) {
this.open();
}
},
/* istanbul ignore next */
activated() {
if (this.shouldReopen) {
this.$emit('input', true);
this.shouldReopen = false;
}
},
beforeDestroy() {
this.removeLock();
removeOverlay(this);
if (this.getContainer) {
removeNode(this.$el);
}
},
/* istanbul ignore next */
deactivated() {
if (this.value) {
this.close();
this.shouldReopen = true;
}
},
methods: {
open() {
/* istanbul ignore next */
if (this.$isServer || this.opened) {
return;
}
// cover default zIndex
if (this.zIndex !== undefined) {
context.zIndex = this.zIndex;
}
this.opened = true;
this.renderOverlay();
this.addLock();
},
addLock() {
if (this.lockScroll) {
on(document, 'touchstart', this.touchStart);
on(document, 'touchmove', this.onTouchMove);
if (!context.lockCount) {
document.body.classList.add('van-overflow-hidden');
}
context.lockCount++;
}
},
removeLock() {
if (this.lockScroll && context.lockCount) {
context.lockCount--;
off(document, 'touchstart', this.touchStart);
off(document, 'touchmove', this.onTouchMove);
if (!context.lockCount) {
document.body.classList.remove('van-overflow-hidden');
}
}
},
close() {
if (!this.opened) {
return;
}
closeOverlay(this);
this.opened = false;
this.removeLock();
this.$emit('input', false);
},
onTouchMove(event) {
this.touchMove(event);
const direction = this.deltaY > 0 ? '10' : '01';
const el = getScroller(event.target, this.$el);
const { scrollHeight, offsetHeight, scrollTop } = el;
let status = '11';
/* istanbul ignore next */
if (scrollTop === 0) {
status = offsetHeight >= scrollHeight ? '00' : '01';
} else if (scrollTop + offsetHeight >= scrollHeight) {
status = '10';
}
/* istanbul ignore next */
if (
status !== '11' &&
this.direction === 'vertical' &&
!(parseInt(status, 2) & parseInt(direction, 2))
) {
preventDefault(event, true);
}
},
renderOverlay() {
if (this.$isServer || !this.value) {
return;
}
this.$nextTick(() => {
this.updateZIndex(this.overlay ? 1 : 0);
if (this.overlay) {
openOverlay(this, {
zIndex: context.zIndex++,
duration: this.duration,
className: this.overlayClass,
customStyle: this.overlayStyle,
});
} else {
closeOverlay(this);
}
});
},
updateZIndex(value = 0) {
this.$el.style.zIndex = ++context.zIndex + value;
},
},
};
}

View File

@ -0,0 +1,77 @@
import Overlay from '../../overlay';
import { context } from './context';
import { mount } from '../../utils/functional';
import { removeNode } from '../../utils/dom/node';
export type OverlayConfig = {
zIndex?: number;
className?: string;
customStyle?: string | object[] | object;
};
const defaultConfig: OverlayConfig = {
className: '',
customStyle: {},
};
function mountOverlay(vm: any) {
return mount(Overlay, {
on: {
// close popup when overlay clicked & closeOnClickOverlay is true
click() {
vm.$emit('click-overlay');
if (vm.closeOnClickOverlay) {
if (vm.onClickOverlay) {
vm.onClickOverlay();
} else {
vm.close();
}
}
},
},
});
}
export function updateOverlay(vm: any): void {
const item = context.find(vm);
if (item) {
const el = vm.$el;
const { config, overlay } = item;
if (el && el.parentNode) {
el.parentNode.insertBefore(overlay.$el, el);
}
Object.assign(overlay, defaultConfig, config, {
show: true,
});
}
}
export function openOverlay(vm: any, config: OverlayConfig): void {
const item = context.find(vm);
if (item) {
item.config = config;
} else {
const overlay = mountOverlay(vm);
context.stack.push({ vm, config, overlay });
}
updateOverlay(vm);
}
export function closeOverlay(vm: any): void {
const item = context.find(vm);
if (item) {
item.overlay.show = false;
}
}
export function removeOverlay(vm: any) {
const item = context.find(vm);
if (item) {
removeNode(item.overlay.$el);
}
}

View File

@ -0,0 +1,13 @@
export type GetContainer = () => Element;
export type PopupMixinProps = {
value: boolean;
zIndex: string | number;
overlay?: boolean;
lockScroll: boolean;
lazyRender: boolean;
overlayClass?: any;
overlayStyle?: object | object[];
getContainer?: string | GetContainer;
closeOnClickOverlay?: boolean;
};

47
src-next/mixins/portal.js Normal file
View File

@ -0,0 +1,47 @@
function getElement(selector) {
if (typeof selector === 'string') {
return document.querySelector(selector);
}
return selector();
}
export function PortalMixin({ ref, afterPortal }) {
return {
props: {
getContainer: [String, Function],
},
watch: {
getContainer: 'portal',
},
mounted() {
if (this.getContainer) {
this.portal();
}
},
methods: {
portal() {
const { getContainer } = this;
const el = ref ? this.$refs[ref] : this.$el;
let container;
if (getContainer) {
container = getElement(getContainer);
} else if (this.$parent) {
container = this.$parent.$el;
}
if (container && container !== el.parentNode) {
container.appendChild(el);
}
if (afterPortal) {
afterPortal.call(this);
}
},
},
};
}

View File

@ -0,0 +1,69 @@
import { sortChildren } from '../utils/vnodes';
export function ChildrenMixin(parent, options = {}) {
const indexKey = options.indexKey || 'index';
return {
inject: {
// TODO: disableBindRelation
parent: {
from: parent,
default: null,
},
},
computed: {
[indexKey]() {
this.bindRelation();
if (this.parent) {
return this.parent.children.indexOf(this);
}
return null;
},
},
mounted() {
this.bindRelation();
},
beforeDestroy() {
if (this.parent) {
this.parent.children = this.parent.children.filter(
(item) => item !== this
);
}
},
methods: {
bindRelation() {
if (!this.parent || this.parent.children.indexOf(this) !== -1) {
return;
}
const children = [...this.parent.children, this];
sortChildren(children, this.parent);
this.parent.children = children;
},
},
};
}
export function ParentMixin(parent) {
return {
provide() {
return {
[parent]: this,
};
},
data() {
return {
children: [],
};
},
};
}

18
src-next/mixins/slots.js Normal file
View File

@ -0,0 +1,18 @@
/**
* Use scopedSlots in Vue 2.6+
* downgrade to slots in lower version
*/
export const SlotsMixin = {
methods: {
slots(name = 'default', props) {
const { $slots, $scopedSlots } = this;
const scopedSlot = $scopedSlots[name];
if (scopedSlot) {
return scopedSlot(props);
}
return $slots[name];
},
},
};

61
src-next/mixins/touch.js Normal file
View File

@ -0,0 +1,61 @@
import { on } from '../utils/dom/event';
const MIN_DISTANCE = 10;
function getDirection(x, y) {
if (x > y && x > MIN_DISTANCE) {
return 'horizontal';
}
if (y > x && y > MIN_DISTANCE) {
return 'vertical';
}
return '';
}
export const TouchMixin = {
data() {
return { direction: '' };
},
methods: {
touchStart(event) {
this.resetTouchStatus();
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
},
touchMove(event) {
const touch = event.touches[0];
this.deltaX = touch.clientX - this.startX;
this.deltaY = touch.clientY - this.startY;
this.offsetX = Math.abs(this.deltaX);
this.offsetY = Math.abs(this.deltaY);
this.direction =
this.direction || getDirection(this.offsetX, this.offsetY);
},
resetTouchStatus() {
this.direction = '';
this.deltaX = 0;
this.deltaY = 0;
this.offsetX = 0;
this.offsetY = 0;
},
// avoid Vue 2.6 event bubble issues by manually binding events
// https://github.com/youzan/vant/issues/3015
bindTouchEvent(el) {
const { onTouchStart, onTouchMove, onTouchEnd } = this;
on(el, 'touchstart', onTouchStart);
on(el, 'touchmove', onTouchMove);
if (onTouchEnd) {
on(el, 'touchend', onTouchEnd);
on(el, 'touchcancel', onTouchEnd);
}
},
},
};

41
src-next/row/index.less Normal file
View File

@ -0,0 +1,41 @@
@import '../style/var';
.van-row {
&::after {
display: table;
clear: both;
content: '';
}
&--flex {
display: flex;
&::after {
display: none;
}
}
&--justify-center {
justify-content: center;
}
&--justify-end {
justify-content: flex-end;
}
&--justify-space-between {
justify-content: space-between;
}
&--justify-space-around {
justify-content: space-around;
}
&--align-center {
align-items: center;
}
&--align-bottom {
align-items: flex-end;
}
}