diff --git a/package.json b/package.json
index 32fba9b95737a..4f71251974865 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@storybook/addon-storysource": "^5.3.19",
"@storybook/html": "^5.3.19",
"@types/jest": "^26.0.7",
+ "@types/lodash-es": "^4.17.3",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"@vue/compiler-sfc": "^3.0.0-rc.1",
@@ -30,9 +31,9 @@
"eslint": "^7.5.0",
"eslint-plugin-vue": "^7.0.0-beta.0",
"husky": "^4.2.5",
- "lint-staged": "^10.2.11",
"jest": "^24.1.0",
"lerna": "^3.22.1",
+ "lint-staged": "^10.2.11",
"ts-jest": "^26.1.3",
"ts-loader": "^8.0.1",
"typescript": "^3.9.7",
@@ -63,5 +64,8 @@
"description": "A Component Library for Vue3.0",
"main": "index.js",
"repository": "git@github.com:element-plus/element-plus.git",
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "lodash-es": "^4.17.15"
+ }
}
diff --git a/packages/utils/click-outside.ts b/packages/utils/click-outside.ts
new file mode 100644
index 0000000000000..2b13be69e8b65
--- /dev/null
+++ b/packages/utils/click-outside.ts
@@ -0,0 +1,83 @@
+import {DirectiveBinding, VNode} from 'vue'
+import isServer from './isServer'
+import { on } from './dom'
+
+const nodeList = []
+const ctx = '@@clickoutsideContext'
+
+let startClick
+let seed = 0
+
+!isServer && on(document, 'mousedown', e => (startClick = e))
+
+!isServer &&
+ on(document, 'mouseup', (e: Event) => {
+ nodeList.forEach(node => node[ctx].documentHandler(e, startClick))
+ })
+
+function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding, vnode: VNode) {
+ return function(mouseup = {}, mousedown = {}) {
+ if (
+ !vnode ||
+ !vnode.component ||
+ !mouseup.target ||
+ !mousedown.target ||
+ el.contains(mouseup.target) ||
+ el.contains(mousedown.target) ||
+ el === mouseup.target ||
+ (vnode.component.popperElm &&
+ (vnode.component.popperElm.contains(mouseup.target) ||
+ vnode.component.popperElm.contains(mousedown.target)))
+ )
+ return
+
+ if (
+ binding.value &&
+ el[ctx].methodName &&
+ vnode.component[el[ctx].methodName]
+ ) {
+ vnode.component[el[ctx].methodName]()
+ } else {
+ el[ctx].bindingFn && el[ctx].bindingFn()
+ }
+ }
+}
+
+/**
+ * v-clickoutside
+ * @desc 点击元素外面才会触发的事件
+ * @example
+ * ```vue
+ *
+ * ```
+ */
+export default {
+ mounted(el: HTMLElement, binding: DirectiveBinding, vnode: VNode):void {
+ nodeList.push(el)
+ const id = seed++
+ el[ctx] = {
+ id,
+ documentHandler: createDocumentHandler(el, binding, vnode),
+ methodName: binding.arg,
+ bindingFn: binding.value,
+ }
+ },
+
+ updated(el: HTMLElement, binding: DirectiveBinding, vnode: VNode): void {
+ el[ctx].documentHandler = createDocumentHandler(el, binding, vnode)
+ el[ctx].methodName = binding.arg
+ el[ctx].bindingFn = binding.value
+ },
+
+ unmounted(el: HTMLElement): void {
+ const len = nodeList.length
+
+ for (let i = 0; i < len; i++) {
+ if (nodeList[i][ctx].id === el[ctx].id) {
+ nodeList.splice(i, 1)
+ break
+ }
+ }
+ delete el[ctx]
+ },
+}
diff --git a/packages/utils/dom.ts b/packages/utils/dom.ts
new file mode 100644
index 0000000000000..f3c4d4ba66a8f
--- /dev/null
+++ b/packages/utils/dom.ts
@@ -0,0 +1,223 @@
+import isServer from './isServer'
+
+const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
+const MOZ_HACK_REGEXP = /^moz([A-Z])/
+
+/* istanbul ignore next */
+const trim = function(s: string) {
+ return (s || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
+}
+/* istanbul ignore next */
+const camelCase = function(name: string) {
+ return name
+ .replace(SPECIAL_CHARS_REGEXP, function(_, __, letter, offset) {
+ return offset ? letter.toUpperCase() : letter
+ })
+ .replace(MOZ_HACK_REGEXP, 'Moz$1')
+}
+
+/* istanbul ignore next */
+export const on = (function() {
+ // Since Vue3 does not support < IE11, we don't need to support it as well.
+ if (!isServer) {
+ return function(
+ element: HTMLElement | Document,
+ event: string,
+ handler: EventListenerOrEventListenerObject,
+ ) {
+ if (element && event && handler) {
+ element.addEventListener(event, handler, false)
+ }
+ }
+ }
+})()
+
+/* istanbul ignore next */
+export const off = (function() {
+ if (!isServer) {
+ return function(
+ element: HTMLElement,
+ event: string,
+ handler: EventListenerOrEventListenerObject,
+ ) {
+ if (element && event) {
+ element.removeEventListener(event, handler, false)
+ }
+ }
+ }
+})()
+
+/* istanbul ignore next */
+export const once = function(
+ el: HTMLElement,
+ event: string,
+ fn: EventListener,
+) {
+ var listener = function() {
+ if (fn) {
+ fn.apply(this, arguments)
+ }
+ off(el, event, listener)
+ }
+ on(el, event, listener)
+}
+
+/* istanbul ignore next */
+export function hasClass(el: HTMLElement, cls: string) {
+ if (!el || !cls) return false
+ if (cls.indexOf(' ') !== -1)
+ throw new Error('className should not contain space.')
+ if (el.classList) {
+ return el.classList.contains(cls)
+ } else {
+ return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
+ }
+}
+
+/* istanbul ignore next */
+export function addClass(el: HTMLElement, cls: string) {
+ if (!el) return
+ let curClass = el.className
+ const classes = (cls || '').split(' ')
+
+ for (let i = 0, j = classes.length; i < j; i++) {
+ const clsName = classes[i]
+ if (!clsName) continue
+
+ if (el.classList) {
+ el.classList.add(clsName)
+ } else if (!hasClass(el, clsName)) {
+ curClass += ' ' + clsName
+ }
+ }
+ if (!el.classList) {
+ el.className = curClass
+ }
+}
+
+/* istanbul ignore next */
+export function removeClass(el: HTMLElement, cls: string) {
+ if (!el || !cls) return
+ const classes = cls.split(' ')
+ let curClass = ' ' + el.className + ' '
+
+ for (let i = 0, j = classes.length; i < j; i++) {
+ const clsName = classes[i]
+ if (!clsName) continue
+
+ if (el.classList) {
+ el.classList.remove(clsName)
+ } else if (hasClass(el, clsName)) {
+ curClass = curClass.replace(' ' + clsName + ' ', ' ')
+ }
+ }
+ if (!el.classList) {
+ el.className = trim(curClass)
+ }
+}
+
+/* istanbul ignore next */
+// Here I want to use the type CSSStyleDeclaration, but the definition for CSSStyleDeclaration
+// has { [index: number]: string } in its type annotation, which does not satisfiy the method
+// camelCase(s: string)
+// Same as the return type
+export const getStyle = function(
+ element: HTMLElement,
+ styleName: string,
+): string {
+ if (isServer) return
+ if (!element || !styleName) return null
+ styleName = camelCase(styleName)
+ if (styleName === 'float') {
+ styleName = 'cssFloat'
+ }
+ try {
+ const computed = document.defaultView.getComputedStyle(element, '')
+ return element.style[styleName] || computed ? computed[styleName] : null
+ } catch (e) {
+ return element.style[styleName]
+ }
+}
+
+/* istanbul ignore next */
+export function setStyle(
+ element: HTMLElement,
+ styleName: CSSStyleDeclaration | string,
+ value: string,
+) {
+ if (!element || !styleName) return
+
+ if (typeof styleName === 'object') {
+ for (const prop in styleName) {
+ if (styleName.hasOwnProperty(prop)) {
+ setStyle(element, prop, styleName[prop])
+ }
+ }
+ } else {
+ styleName = camelCase(styleName)
+
+ element.style[styleName] = value
+ }
+}
+
+export const isScroll = (el: HTMLElement, isVertical?: Nullable
) => {
+ if (isServer) return
+
+ const determinedDirection = isVertical !== null || isVertical !== undefined
+ const overflow = determinedDirection
+ ? isVertical
+ ? getStyle(el, 'overflow-y')
+ : getStyle(el, 'overflow-x')
+ : getStyle(el, 'overflow')
+
+ return overflow.match(/(scroll|auto)/)
+}
+
+export const getScrollContainer = (
+ el: HTMLElement,
+ isVertical?: Nullable,
+) => {
+ if (isServer) return
+ el.classList
+ let parent: HTMLElement = el
+ while (parent) {
+ if ([window, document, document.documentElement].includes(parent)) {
+ return window
+ }
+ if (isScroll(parent, isVertical)) {
+ return parent
+ }
+ parent = parent.parentNode as HTMLElement
+ }
+
+ return parent
+}
+
+export const isInContainer = (el: HTMLElement, container: HTMLElement) => {
+ if (isServer || !el || !container) return false
+
+ const elRect = el.getBoundingClientRect()
+ let containerRect: Partial
+
+ if (
+ [window, document, document.documentElement, null, undefined].includes(
+ container,
+ )
+ ) {
+ containerRect = {
+ top: 0,
+ right: window.innerWidth,
+ bottom: window.innerHeight,
+ left: 0,
+ }
+ } else {
+ containerRect = container.getBoundingClientRect()
+ }
+
+ return (
+ elRect.top < containerRect.bottom &&
+ elRect.bottom > containerRect.top &&
+ elRect.right > containerRect.left &&
+ elRect.left < containerRect.right
+ )
+}
diff --git a/packages/utils/isDef.ts b/packages/utils/isDef.ts
new file mode 100644
index 0000000000000..38fca34c7298c
--- /dev/null
+++ b/packages/utils/isDef.ts
@@ -0,0 +1,7 @@
+export function isDef(val) {
+ return val !== undefined && val !== null
+}
+export function isKorean(text: string) {
+ const reg = /([(\uAC00-\uD7AF)|(\u3130-\u318F)])+/gi
+ return reg.test(text)
+}
diff --git a/packages/utils/isServer.ts b/packages/utils/isServer.ts
new file mode 100644
index 0000000000000..eff99860b625e
--- /dev/null
+++ b/packages/utils/isServer.ts
@@ -0,0 +1 @@
+export default typeof window === undefined
diff --git a/packages/utils/merge.ts b/packages/utils/merge.ts
new file mode 100644
index 0000000000000..4b9b296b8d2f9
--- /dev/null
+++ b/packages/utils/merge.ts
@@ -0,0 +1,7 @@
+export default function(target: Object, ...args: Array