From 12faaa21795c8a9f918e4e363b2d024a1cc133d3 Mon Sep 17 00:00:00 2001 From: chenjiahan Date: Sun, 5 Jul 2020 08:32:49 +0800 Subject: [PATCH] chore: move utils --- src-next/utils/constant.ts | 14 +++ src-next/utils/create/i18n.ts | 4 +- src-next/utils/deep-assign.ts | 27 ++++++ src-next/utils/deep-clone.ts | 13 +++ src-next/utils/dom/event.ts | 56 ++++++++++++ src-next/utils/dom/node.ts | 7 ++ src-next/utils/dom/raf.ts | 40 ++++++++ src-next/utils/dom/reset-scroll.ts | 16 ++++ src-next/utils/dom/scroll.ts | 90 ++++++++++++++++++ src-next/utils/dom/style.ts | 11 +++ src-next/utils/format/number.ts | 31 +++++++ src-next/utils/format/string.ts | 15 +++ src-next/utils/format/unit.ts | 43 +++++++++ src-next/utils/functional.ts | 71 +++++++++++++++ src-next/utils/index.ts | 32 +++++++ src-next/utils/router.ts | 54 +++++++++++ src-next/utils/test/bem.spec.js | 39 ++++++++ src-next/utils/test/index.spec.js | 142 +++++++++++++++++++++++++++++ src-next/utils/types.ts | 40 ++++++++ src-next/utils/vnodes.ts | 33 +++++++ 20 files changed, 776 insertions(+), 2 deletions(-) create mode 100644 src-next/utils/constant.ts create mode 100644 src-next/utils/deep-assign.ts create mode 100644 src-next/utils/deep-clone.ts create mode 100644 src-next/utils/dom/event.ts create mode 100644 src-next/utils/dom/node.ts create mode 100644 src-next/utils/dom/raf.ts create mode 100644 src-next/utils/dom/reset-scroll.ts create mode 100644 src-next/utils/dom/scroll.ts create mode 100644 src-next/utils/dom/style.ts create mode 100644 src-next/utils/format/number.ts create mode 100644 src-next/utils/format/string.ts create mode 100644 src-next/utils/format/unit.ts create mode 100644 src-next/utils/functional.ts create mode 100644 src-next/utils/index.ts create mode 100644 src-next/utils/router.ts create mode 100644 src-next/utils/test/bem.spec.js create mode 100644 src-next/utils/test/index.spec.js create mode 100644 src-next/utils/types.ts create mode 100644 src-next/utils/vnodes.ts diff --git a/src-next/utils/constant.ts b/src-next/utils/constant.ts new file mode 100644 index 000000000..eb6563daf --- /dev/null +++ b/src-next/utils/constant.ts @@ -0,0 +1,14 @@ +// color +export const RED = '#ee0a24'; +export const BLUE = '#1989fa'; +export const GREEN = '#07c160'; +export const WHITE = '#fff'; + +// border +export const BORDER = 'van-hairline'; +export const BORDER_TOP = `${BORDER}--top`; +export const BORDER_LEFT = `${BORDER}--left`; +export const BORDER_BOTTOM = `${BORDER}--bottom`; +export const BORDER_SURROUND = `${BORDER}--surround`; +export const BORDER_TOP_BOTTOM = `${BORDER}--top-bottom`; +export const BORDER_UNSET_TOP_BOTTOM = `${BORDER}-unset--top-bottom`; diff --git a/src-next/utils/create/i18n.ts b/src-next/utils/create/i18n.ts index 7236fbc6b..37da6501d 100644 --- a/src-next/utils/create/i18n.ts +++ b/src-next/utils/create/i18n.ts @@ -1,5 +1,5 @@ -import { get, isFunction } from '../../../src/utils'; -import { camelize } from '../../../src/utils/format/string'; +import { get, isFunction } from '..'; +import { camelize } from '../format/string'; import locale from '../../locale'; export function createI18N(name: string) { diff --git a/src-next/utils/deep-assign.ts b/src-next/utils/deep-assign.ts new file mode 100644 index 000000000..6a175c57a --- /dev/null +++ b/src-next/utils/deep-assign.ts @@ -0,0 +1,27 @@ +import { isDef, isObject } from '.'; +import { ObjectIndex } from './types'; + +const { hasOwnProperty } = Object.prototype; + +function assignKey(to: ObjectIndex, from: ObjectIndex, key: string) { + const val = from[key]; + + if (!isDef(val)) { + return; + } + + if (!hasOwnProperty.call(to, key) || !isObject(val)) { + to[key] = val; + } else { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + to[key] = deepAssign(Object(to[key]), from[key]); + } +} + +export function deepAssign(to: ObjectIndex, from: ObjectIndex): ObjectIndex { + Object.keys(from).forEach((key) => { + assignKey(to, from, key); + }); + + return to; +} diff --git a/src-next/utils/deep-clone.ts b/src-next/utils/deep-clone.ts new file mode 100644 index 000000000..254b8e6b1 --- /dev/null +++ b/src-next/utils/deep-clone.ts @@ -0,0 +1,13 @@ +import { deepAssign } from './deep-assign'; + +export function deepClone(obj: object): object { + if (Array.isArray(obj)) { + return obj.map((item) => deepClone(item)); + } + + if (typeof obj === 'object') { + return deepAssign({}, obj); + } + + return obj; +} diff --git a/src-next/utils/dom/event.ts b/src-next/utils/dom/event.ts new file mode 100644 index 000000000..459e581f1 --- /dev/null +++ b/src-next/utils/dom/event.ts @@ -0,0 +1,56 @@ +import { isServer } from '..'; +import { EventHandler } from '../types'; + +// eslint-disable-next-line import/no-mutable-exports +export let supportsPassive = false; + +if (!isServer) { + try { + const opts = {}; + Object.defineProperty(opts, 'passive', { + // eslint-disable-next-line getter-return + get() { + /* istanbul ignore next */ + supportsPassive = true; + }, + }); + window.addEventListener('test-passive', null as any, opts); + // eslint-disable-next-line no-empty + } catch (e) {} +} + +export function on( + target: EventTarget, + event: string, + handler: EventHandler, + passive = false +) { + if (!isServer) { + target.addEventListener( + event, + handler, + supportsPassive ? { capture: false, passive } : false + ); + } +} + +export function off(target: EventTarget, event: string, handler: EventHandler) { + if (!isServer) { + target.removeEventListener(event, handler); + } +} + +export function stopPropagation(event: Event) { + event.stopPropagation(); +} + +export function preventDefault(event: Event, isStopPropagation?: boolean) { + /* istanbul ignore else */ + if (typeof event.cancelable !== 'boolean' || event.cancelable) { + event.preventDefault(); + } + + if (isStopPropagation) { + stopPropagation(event); + } +} diff --git a/src-next/utils/dom/node.ts b/src-next/utils/dom/node.ts new file mode 100644 index 000000000..07756867a --- /dev/null +++ b/src-next/utils/dom/node.ts @@ -0,0 +1,7 @@ +export function removeNode(el: Node) { + const parent = el.parentNode; + + if (parent) { + parent.removeChild(el); + } +} diff --git a/src-next/utils/dom/raf.ts b/src-next/utils/dom/raf.ts new file mode 100644 index 000000000..fa32db10b --- /dev/null +++ b/src-next/utils/dom/raf.ts @@ -0,0 +1,40 @@ +/** + * requestAnimationFrame polyfill + */ + +import { isServer } from '..'; + +let prev = Date.now(); + +/* istanbul ignore next */ +function fallback(fn: FrameRequestCallback): number { + const curr = Date.now(); + const ms = Math.max(0, 16 - (curr - prev)); + const id = setTimeout(fn, ms); + prev = curr + ms; + return id; +} + +/* istanbul ignore next */ +const root = (isServer ? global : window) as Window; + +/* istanbul ignore next */ +const iRaf = root.requestAnimationFrame || fallback; + +/* istanbul ignore next */ +const iCancel = root.cancelAnimationFrame || root.clearTimeout; + +export function raf(fn: FrameRequestCallback): number { + return iRaf.call(root, fn); +} + +// double raf for animation +export function doubleRaf(fn: FrameRequestCallback): void { + raf(() => { + raf(fn); + }); +} + +export function cancelRaf(id: number) { + iCancel.call(root, id); +} diff --git a/src-next/utils/dom/reset-scroll.ts b/src-next/utils/dom/reset-scroll.ts new file mode 100644 index 000000000..8cdf18a40 --- /dev/null +++ b/src-next/utils/dom/reset-scroll.ts @@ -0,0 +1,16 @@ +/** + * Hack for iOS12 page scroll + * https://developers.weixin.qq.com/community/develop/doc/00044ae90742f8c82fb78fcae56800 + */ + +import { isIOS as checkIsIOS } from '../validate/system'; +import { getRootScrollTop, setRootScrollTop } from './scroll'; + +const isIOS = checkIsIOS(); + +/* istanbul ignore next */ +export function resetScroll() { + if (isIOS) { + setRootScrollTop(getRootScrollTop()); + } +} diff --git a/src-next/utils/dom/scroll.ts b/src-next/utils/dom/scroll.ts new file mode 100644 index 000000000..aa3f7f571 --- /dev/null +++ b/src-next/utils/dom/scroll.ts @@ -0,0 +1,90 @@ +type ScrollElement = HTMLElement | Window; + +function isWindow(val: unknown): val is Window { + return val === window; +} + +// get nearest scroll element +// http://w3help.org/zh-cn/causes/SD9013 +// http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome +const overflowScrollReg = /scroll|auto/i; +export function getScroller(el: HTMLElement, root: ScrollElement = window) { + let node = el; + + while ( + node && + node.tagName !== 'HTML' && + node.nodeType === 1 && + node !== root + ) { + const { overflowY } = window.getComputedStyle(node); + + if (overflowScrollReg.test(overflowY)) { + if (node.tagName !== 'BODY') { + return node; + } + + // see: https://github.com/youzan/vant/issues/3823 + const { overflowY: htmlOverflowY } = window.getComputedStyle( + node.parentNode as Element + ); + + if (overflowScrollReg.test(htmlOverflowY)) { + return node; + } + } + node = node.parentNode as HTMLElement; + } + + return root; +} + +export function getScrollTop(el: ScrollElement): number { + return 'scrollTop' in el ? el.scrollTop : el.pageYOffset; +} + +export function setScrollTop(el: ScrollElement, value: number) { + if ('scrollTop' in el) { + el.scrollTop = value; + } else { + el.scrollTo(el.scrollX, value); + } +} + +export function getRootScrollTop(): number { + return ( + window.pageYOffset || + document.documentElement.scrollTop || + document.body.scrollTop || + 0 + ); +} + +export function setRootScrollTop(value: number) { + setScrollTop(window, value); + setScrollTop(document.body, value); +} + +// get distance from element top to page top or scroller top +export function getElementTop(el: ScrollElement, scroller?: HTMLElement) { + if (isWindow(el)) { + return 0; + } + + const scrollTop = scroller ? getScrollTop(scroller) : getRootScrollTop(); + return el.getBoundingClientRect().top + scrollTop; +} + +export function getVisibleHeight(el: ScrollElement) { + if (isWindow(el)) { + return el.innerHeight; + } + return el.getBoundingClientRect().height; +} + +export function getVisibleTop(el: ScrollElement) { + if (isWindow(el)) { + return 0; + } + return el.getBoundingClientRect().top; +} diff --git a/src-next/utils/dom/style.ts b/src-next/utils/dom/style.ts new file mode 100644 index 000000000..e3fe897b1 --- /dev/null +++ b/src-next/utils/dom/style.ts @@ -0,0 +1,11 @@ +export function isHidden(el: HTMLElement) { + const style = window.getComputedStyle(el); + const hidden = style.display === 'none'; + + // offsetParent returns null in the following situations: + // 1. The element or its parent element has the display property set to none. + // 2. The element has the position property set to fixed + const parentHidden = el.offsetParent === null && style.position !== 'fixed'; + + return hidden || parentHidden; +} diff --git a/src-next/utils/format/number.ts b/src-next/utils/format/number.ts new file mode 100644 index 000000000..85ca889b5 --- /dev/null +++ b/src-next/utils/format/number.ts @@ -0,0 +1,31 @@ +export function range(num: number, min: number, max: number): number { + return Math.min(Math.max(num, min), max); +} + +function trimExtraChar(value: string, char: string, regExp: RegExp) { + const index = value.indexOf(char); + + if (index === -1) { + return value; + } + + if (char === '-' && index !== 0) { + return value.slice(0, index); + } + + return value.slice(0, index + 1) + value.slice(index).replace(regExp, ''); +} + +export function formatNumber(value: string, allowDot?: boolean) { + if (allowDot) { + value = trimExtraChar(value, '.', /\./g); + } else { + value = value.split('.')[0]; + } + + value = trimExtraChar(value, '-', /-/g); + + const regExp = allowDot ? /[^-0-9.]/g : /[^-0-9]/g; + + return value.replace(regExp, ''); +} diff --git a/src-next/utils/format/string.ts b/src-next/utils/format/string.ts new file mode 100644 index 000000000..f801d910b --- /dev/null +++ b/src-next/utils/format/string.ts @@ -0,0 +1,15 @@ +const camelizeRE = /-(\w)/g; + +export function camelize(str: string): string { + return str.replace(camelizeRE, (_, c) => c.toUpperCase()); +} + +export function padZero(num: number | string, targetLength = 2): string { + let str = num + ''; + + while (str.length < targetLength) { + str = '0' + str; + } + + return str; +} diff --git a/src-next/utils/format/unit.ts b/src-next/utils/format/unit.ts new file mode 100644 index 000000000..b50167e7c --- /dev/null +++ b/src-next/utils/format/unit.ts @@ -0,0 +1,43 @@ +import { isDef } from '..'; +import { isNumeric } from '../validate/number'; + +export function addUnit(value?: string | number): string | undefined { + if (!isDef(value)) { + return undefined; + } + + value = String(value); + return isNumeric(value) ? `${value}px` : value; +} + +// cache +let rootFontSize: number; + +function getRootFontSize() { + if (!rootFontSize) { + const doc = document.documentElement; + const fontSize = + doc.style.fontSize || window.getComputedStyle(doc).fontSize; + + rootFontSize = parseFloat(fontSize); + } + + return rootFontSize; +} + +function convertRem(value: string) { + value = value.replace(/rem/g, ''); + return +value * getRootFontSize(); +} + +export function unitToPx(value: string | number): number { + if (typeof value === 'number') { + return value; + } + + if (value.indexOf('rem') !== -1) { + return convertRem(value); + } + + return parseFloat(value); +} diff --git a/src-next/utils/functional.ts b/src-next/utils/functional.ts new file mode 100644 index 000000000..b2ef48688 --- /dev/null +++ b/src-next/utils/functional.ts @@ -0,0 +1,71 @@ +import Vue, { RenderContext, VNodeData } from 'vue'; +import { ObjectIndex } from './types'; + +type Context = RenderContext & { data: VNodeData & ObjectIndex }; + +type InheritContext = Partial & ObjectIndex; + +const inheritKey = [ + 'ref', + 'style', + 'class', + 'attrs', + 'nativeOn', + 'directives', + 'staticClass', + 'staticStyle', +]; + +const mapInheritKey: ObjectIndex = { nativeOn: 'on' }; + +// inherit partial context, map nativeOn to on +export function inherit( + context: Context, + inheritListeners?: boolean +): InheritContext { + const result = inheritKey.reduce((obj, key) => { + if (context.data[key]) { + obj[mapInheritKey[key] || key] = context.data[key]; + } + return obj; + }, {} as InheritContext); + + if (inheritListeners) { + result.on = result.on || {}; + Object.assign(result.on, context.data.on); + } + + return result; +} + +// emit event +export function emit(context: Context, eventName: string, ...args: any[]) { + const listeners = context.listeners[eventName]; + if (listeners) { + if (Array.isArray(listeners)) { + listeners.forEach((listener) => { + listener(...args); + }); + } else { + listeners(...args); + } + } +} + +// mount functional component +export function mount(Component: any, data?: VNodeData) { + const instance = new Vue({ + el: document.createElement('div'), + props: Component.props, + render(h) { + return h(Component, { + props: this.$props, + ...data, + }); + }, + }); + + document.body.appendChild(instance.$el); + + return instance; +} diff --git a/src-next/utils/index.ts b/src-next/utils/index.ts new file mode 100644 index 000000000..f1a30475f --- /dev/null +++ b/src-next/utils/index.ts @@ -0,0 +1,32 @@ +export { addUnit } from './format/unit'; +export { createNamespace } from './create'; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export function noop() {} + +export function isDef(val: unknown): boolean { + return val !== undefined && val !== null; +} + +export function isFunction(val: unknown): val is Function { + return typeof val === 'function'; +} + +export function isObject(val: unknown): val is Record { + return val !== null && typeof val === 'object'; +} + +export function isPromise(val: unknown): val is Promise { + return isObject(val) && isFunction(val.then) && isFunction(val.catch); +} + +export function get(object: any, path: string): any { + const keys = path.split('.'); + let result = object; + + keys.forEach((key) => { + result = isDef(result[key]) ? result[key] : ''; + }); + + return result; +} diff --git a/src-next/utils/router.ts b/src-next/utils/router.ts new file mode 100644 index 000000000..7a005e4e9 --- /dev/null +++ b/src-next/utils/router.ts @@ -0,0 +1,54 @@ +/** + * Vue Router support + */ + +import { RenderContext } from 'vue/types'; +import VueRouter, { RawLocation } from 'vue-router/types'; + +export type RouteConfig = { + url?: string; + to?: RawLocation; + replace?: boolean; +}; + +function isRedundantNavigation(err: Error) { + return ( + err.name === 'NavigationDuplicated' || + // compatible with vue-router@3.3 + (err.message && err.message.indexOf('redundant navigation') !== -1) + ); +} + +export function route(router: VueRouter, config: RouteConfig) { + const { to, url, replace } = config; + if (to && router) { + const promise = router[replace ? 'replace' : 'push'](to); + + /* istanbul ignore else */ + if (promise && promise.catch) { + promise.catch((err) => { + if (err && !isRedundantNavigation(err)) { + throw err; + } + }); + } + } else if (url) { + replace ? location.replace(url) : (location.href = url); + } +} + +export function functionalRoute(context: RenderContext) { + route(context.parent && context.parent.$router, context.props); +} + +export type RouteProps = { + url?: string; + replace?: boolean; + to?: RawLocation; +}; + +export const routeProps = { + url: String, + replace: Boolean, + to: [String, Object], +}; diff --git a/src-next/utils/test/bem.spec.js b/src-next/utils/test/bem.spec.js new file mode 100644 index 000000000..d0e86466f --- /dev/null +++ b/src-next/utils/test/bem.spec.js @@ -0,0 +1,39 @@ +import { createBEM } from '../create/bem'; + +test('bem', () => { + const bem = createBEM('button'); + + expect(bem()).toEqual('button'); + + expect(bem('text')).toEqual('button__text'); + + expect(bem({ disabled: false })).toEqual('button'); + + expect(bem({ disabled: true })).toEqual('button button--disabled'); + + expect(bem('text', { disabled: true })).toEqual( + 'button__text button__text--disabled' + ); + + expect(bem(['disabled', 'primary'])).toEqual( + 'button button--disabled button--primary' + ); + + expect(bem([])).toEqual('button'); + + expect(bem(null)).toEqual('button'); + + expect(bem([null])).toEqual('button'); + + expect(bem(['disabled', ''])).toEqual('button button--disabled'); + + expect(bem(['disabled', undefined])).toEqual('button button--disabled'); + + expect(bem('text', ['disabled', 'primary'])).toEqual( + 'button__text button__text--disabled button__text--primary' + ); + + expect(bem('text', [{ disabled: true }, 'primary'])).toEqual( + 'button__text button__text--disabled button__text--primary' + ); +}); diff --git a/src-next/utils/test/index.spec.js b/src-next/utils/test/index.spec.js new file mode 100644 index 000000000..a9d6bea10 --- /dev/null +++ b/src-next/utils/test/index.spec.js @@ -0,0 +1,142 @@ +import { deepClone } from '../deep-clone'; +import { deepAssign } from '../deep-assign'; +import { isDef, get, noop } from '..'; +import { raf, cancelRaf } from '../dom/raf'; +import { later } from '../../../test'; +import { isEmail } from '../validate/email'; +import { isMobile } from '../validate/mobile'; +import { isNumeric } from '../validate/number'; +import { isAndroid } from '../validate/system'; +import { camelize } from '../format/string'; +import { formatNumber } from '../format/number'; +import { addUnit, unitToPx } from '../format/unit'; + +test('deepClone', () => { + const a = { foo: 0 }; + const b = { foo: 0, bar: 1 }; + const arr = [a, b]; + expect(deepClone(a)).toEqual(a); + expect(deepClone(b)).toEqual(b); + expect(deepClone(noop)).toEqual(noop); + expect(deepClone(arr)).toEqual(arr); + expect(deepClone(undefined)).toEqual(undefined); + expect(deepClone(1)).toEqual(1); +}); + +test('deepAssign', () => { + expect(deepAssign({}, { foo: null })).toEqual({}); + expect(deepAssign({}, { foo: undefined })).toEqual({}); + expect(deepAssign({ noop: null }, { noop })).toEqual({ noop }); + expect(deepAssign({ foo: 0 }, { bar: 1 })).toEqual({ foo: 0, bar: 1 }); + expect( + deepAssign({ foo: { bar: false } }, { foo: { bar: true, foo: false } }) + ).toEqual({ + foo: { + bar: true, + foo: false, + }, + }); +}); + +test('isDef', () => { + expect(isDef(null)).toBeFalsy(); + expect(isDef(undefined)).toBeFalsy(); + expect(isDef(1)).toBeTruthy(); + expect(isDef('1')).toBeTruthy(); + expect(isDef({})).toBeTruthy(); + expect(isDef(noop)).toBeTruthy(); +}); + +test('camelize', () => { + expect(camelize('ab')).toEqual('ab'); + expect(camelize('a-b')).toEqual('aB'); + expect(camelize('a-b-c-d')).toEqual('aBCD'); + expect(camelize('a-b-')).toEqual('aB-'); + expect(camelize('-a-b')).toEqual('AB'); + expect(camelize('-')).toEqual('-'); +}); + +test('get', () => { + expect(get({ a: 1 }, 'a')).toEqual(1); + expect(get({ a: { b: 2 } }, 'a.b')).toEqual(2); + expect(get({ a: { b: 2 } }, 'a.b.c')).toEqual(''); +}); + +test('isAndroid', () => { + expect(isAndroid()).toBeFalsy(); +}); + +test('raf', async () => { + const spy = jest.fn(); + raf(spy); + + await later(50); + expect(spy).toHaveBeenCalledTimes(1); + cancelRaf(1); +}); + +test('isEmail', () => { + expect(isEmail('abc@gmail.com')).toBeTruthy(); + expect(isEmail('abc@@gmail.com')).toBeFalsy(); + expect(isEmail('@gmail.com')).toBeFalsy(); + expect(isEmail('abc@')).toBeFalsy(); +}); + +test('isMobile', () => { + expect(isMobile('13000000000')).toBeTruthy(); + expect(isMobile('+8613000000000')).toBeTruthy(); + expect(isMobile('8613000000000')).toBeTruthy(); + expect(isMobile('1300000000')).toBeFalsy(); + expect(isMobile('abc')).toBeFalsy(); +}); + +test('isNumeric', () => { + expect(isNumeric('1')).toBeTruthy(); + expect(isNumeric('1.2')).toBeTruthy(); + expect(isNumeric('1..2')).toBeFalsy(); + expect(isNumeric('abc')).toBeFalsy(); + expect(isNumeric('1b2')).toBeFalsy(); +}); + +test('formatNumber', () => { + expect(formatNumber('abc')).toEqual(''); + expect(formatNumber('1.2')).toEqual('1'); + expect(formatNumber('abc1.2')).toEqual('1'); + expect(formatNumber('123.4.')).toEqual('123'); + + // with dot + expect(formatNumber('abc', true)).toEqual(''); + expect(formatNumber('1.2', true)).toEqual('1.2'); + expect(formatNumber('abc1.2', true)).toEqual('1.2'); + expect(formatNumber('123.4.', true)).toEqual('123.4'); + + // minus + expect(formatNumber('-1.2')).toEqual('-1'); + expect(formatNumber('-1.2', true)).toEqual('-1.2'); + expect(formatNumber('-1.2-', true)).toEqual('-1.2'); + expect(formatNumber('123-')).toEqual('123'); +}); + +test('addUnit', () => { + expect(addUnit(0)).toEqual('0px'); + expect(addUnit(10)).toEqual('10px'); + expect(addUnit('1%')).toEqual('1%'); + expect(addUnit('1px')).toEqual('1px'); + expect(addUnit('1vw')).toEqual('1vw'); + expect(addUnit('1vh')).toEqual('1vh'); + expect(addUnit('1rem')).toEqual('1rem'); +}); + +test('unitToPx', () => { + const originGetComputedStyle = window.getComputedStyle; + + window.getComputedStyle = () => ({ fontSize: '16px' }); + + expect(unitToPx(0)).toEqual(0); + expect(unitToPx(10)).toEqual(10); + expect(unitToPx('10px')).toEqual(10); + expect(unitToPx('0rem')).toEqual(0); + expect(unitToPx('10rem')).toEqual(160); + + window.getComputedStyle = originGetComputedStyle; +}); diff --git a/src-next/utils/types.ts b/src-next/utils/types.ts new file mode 100644 index 000000000..14821d4b6 --- /dev/null +++ b/src-next/utils/types.ts @@ -0,0 +1,40 @@ +import { VNode, CreateElement, RenderContext } from 'vue'; +import { InjectOptions, PropsDefinition } from 'vue/types/options'; + +export type EventHandler = (event: Event) => void; + +export type ObjectIndex = Record; + +export type ScopedSlot = ( + props?: Props +) => VNode[] | VNode | undefined; + +export type DefaultSlots = { + default?: ScopedSlot; +}; + +export type ScopedSlots = DefaultSlots & { + [key: string]: ScopedSlot | undefined; +}; + +export type ModelOptions = { + prop?: string; + event?: string; +}; + +export type DefaultProps = ObjectIndex; + +export type FunctionComponent< + Props = DefaultProps, + PropDefs = PropsDefinition +> = { + ( + h: CreateElement, + props: Props, + slots: ScopedSlots, + context: RenderContext + ): VNode | undefined; + props?: PropDefs; + model?: ModelOptions; + inject?: InjectOptions; +}; diff --git a/src-next/utils/vnodes.ts b/src-next/utils/vnodes.ts new file mode 100644 index 000000000..471d2ea38 --- /dev/null +++ b/src-next/utils/vnodes.ts @@ -0,0 +1,33 @@ +import { VNode } from 'vue'; + +function flattenVNodes(vnodes: VNode[]) { + const result: VNode[] = []; + + function traverse(vnodes: VNode[]) { + vnodes.forEach((vnode) => { + result.push(vnode); + + if (vnode.componentInstance) { + traverse(vnode.componentInstance.$children.map((item) => item.$vnode)); + } + + if (vnode.children) { + traverse(vnode.children); + } + }); + } + + traverse(vnodes); + return result; +} + +// sort children instances by vnodes order +export function sortChildren(children: Vue[], parent: Vue) { + const { componentOptions } = parent.$vnode; + if (!componentOptions || !componentOptions.children) { + return; + } + + const vnodes = flattenVNodes(componentOptions.children); + children.sort((a, b) => vnodes.indexOf(a.$vnode) - vnodes.indexOf(b.$vnode)); +}