I. Introduction of instructions

As the name suggests, the click-outside directive is what happens when you click outside of an element node.

The common use is to close the popup when clicking outside of the popup box, and is used in the Elder-Plus source code for components such as el-select/el-color-picker.

Let’s first look at the code used in el-select:

// packages\components\select\src\select.vue
<div
    ref="selectWrapper"
    v-click-outside:[popperPaneRef] ="handleClose"
    class="el-select"
    :class="[selectSize ? 'el-select--' + selectSize : '']"
    @click.stop="toggleMenu"
  >
  <el-popper .>
  </el-popper>
 </div>
 
 
Copy the code

The above code uses the instruction:

  • An ARG (parameter) is passed in[popperPaneRef], which is an array type whose elements are nodes, indicating that clicking on these nodes does not trigger the click-outside callback;
  • Binds a value:handleClose, is a function that is used as a click-outside callback to close the pop-up box.

Two, command source code analysis

import { on } from "@element-plus/utils/dom";
import isServer from "@element-plus/utils/isServer";

import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from "vue";
import type { Nullable } from "@element-plus/utils/types";

// Type definition
typeDocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void; type FlushList = Map< HTMLElement, { documentHandler: DocumentHandler; bindingFn: (... args: unknown[]) => unknown; } [] >; Handlers const nodeList: FlushList = new map (); // Store the mouseDown event. // if (! On (document, "mouseDown ", (e: MouseEvent) => (startClick = e)); On (document, "mouseup", (e: Handlers for (const handlers of nodelist.values ()) {handlers for (const { {documentHandler} of handlers} {documentHandler(e, startClick) {// Execute each handler, passing in parameters like mouseUp event, MouseDown event documentHandler(e, startClick); }}}); } // Create a handler function createDocument (el: HTMLElement, binding: DirectiveBinding): DocumentHandler {// Store node let excludes: HTMLElement[] = []; // If (array.isarray (binding.arg)) {excludes = binding.arg; } else if ((binding.arg as unknown) instanceof HTMLElement) { // due to current implementation on binding type is wrong the type casting is necessary here excludes.push((binding.arg as unknown) as HTMLElement); } // can be read as the function curry // The handler that is actually executed is the mouseup mousedown event that is triggered when clicked. Mousedown) {// popperRef is an element in the popper section of the el-popper component (binding.instance as ComponentPublicInstance<{ popperRef: Nullable<HTMLElement>; }>).popperRef; const mouseUpTarget = mouseup.target as Node; const mouseDownTarget = mousedown? .target as Node; Const isBound =! binding || ! binding.instance; // Whether the target element that triggers mouseup/mousedown exists const isTargetExists =! mouseUpTarget || ! mouseDownTarget; / / trigger element is included in the instruction of binding element internal const isContainedByEl = el. The contains (mouseUpTarget) | | el. The contains (mouseDownTarget); Const isSelf = el === mouseUpTarget; // Triggered element should be excluded const isTargetExcluded = (excludes. Length && Excludes. Some ((item) => item? .contains(mouseUpTarget))) || (excludes.length && excludes.includes(mouseDownTarget as HTMLElement)); / / click on the element is in popper section contains the elements of the const isContainedByPopper = popperRef && (popperRef. The contains (mouseUpTarget) | | popperRef.contains(mouseDownTarget)); // If one of the above conditions is true, Does not perform the callback function if (isBound | | isTargetExists | | isContainedByEl | | isSelf | | isTargetExcluded | | isContainedByPopper) {return;  } // Execute the callback function binding.value(mouseup, mousedown); }; } // const ClickOutside: ObjectDirective = {/ / instruction hook function, parameters can refer to the official document / / el: https://vue3js.cn/docs/zh/api/application-api.html#directive The DOM element node of the directive binding contains multiple object attributes, and // -instance is the component instance using the directive; // -value: the value passed to the directive, in this case the click-outside callback; // -oldValue: previous value, available only in beforeUpdate and updated. // -arg: parameter passed to the instruction; / /... BeforeMount (el, binding) {// Multiple event handlers may be bound on a node // So nodeList map value is array type if (! nodeList.has(el)) { nodeList.set(el, []); } // Create an event response function and push it into the hanlders array of the current node nodelist.get (el). Push ({documentHandler: createDocumentHandler(el, binding), bindingFn: binding.value, }); }, // The hook updated(el, binding) {if (! nodeList.has(el)) { nodeList.set(el, []); } const handlers = nodeList.get(el); Handlers. FindIndex ((item) => item.bindingFn === binding.oldValue); Const newHandler = {documentHandler: createDocumentHandler(el, binding), bindingFn: binding. Value,}; Handlers. Splice (oldHandlerIndex, 1, newHandler); } else {// otherwise just push newHandler. Push (newHandler); }}, unmounted(el) {// Delete nodelist.delete (el) from nodeList when the node is unmounted. }}; export default ClickOutside;Copy the code

Iii. Summary:

  1. A global variable, nodeList, is used to store nodes and their handlers and is passed when the nodes beforeMountcreateDocumentHandlerCreate a handler and store it in nodeList.
  2. usemousedown mouseupThe mousedown response uses startClick to store the event, the Mouseup event traverses the nodeList node, and the handlers of the node, executing the handlers in turn.
  3. createDocumentHandlerInside the function, we use curry to preserve the el and binding that were passed in when the handler was created. When called by the handler, we only need to pass in the mouseDown and mouseup event objects.
  4. In hanlder, a series of scenarios are judged that do not need to trigger the callback function