mldong-flow-designer-plus
Version:
本项目包含了作者为B站课堂视频[《工作流设计器开发最佳实践》](https://www.bilibili.com/cheese/play/ss24484)的过程源码。教程中开发的组件也可用于实际生产环境中。以下是和使用文档和课程章节说明。 ## 实战项目 [演示地址](https://flow-pro.mldong.com/)
484 lines (483 loc) • 16.3 kB
JavaScript
import { ref, onMounted, onBeforeUnmount, defineComponent, provide, watch, unref, nextTick, renderSlot, openBlock, createBlock, Teleport as Teleport$1 } from "vue";
import { o as isElement, h as isClient, _ as _export_sfc, c as isString, b as buildProps, d as definePropType, w as withInstall } from "./install-La2G4dPY.js";
import { E as EVENT_CODE } from "./aria-DJRDfGoO.js";
import { j as isNil } from "./index-BLhyZWKY.js";
const FOCUS_AFTER_TRAPPED = "focus-trap.focus-after-trapped";
const FOCUS_AFTER_RELEASED = "focus-trap.focus-after-released";
const FOCUSOUT_PREVENTED = "focus-trap.focusout-prevented";
const FOCUS_AFTER_TRAPPED_OPTS = {
cancelable: true,
bubbles: false
};
const FOCUSOUT_PREVENTED_OPTS = {
cancelable: true,
bubbles: false
};
const ON_TRAP_FOCUS_EVT = "focusAfterTrapped";
const ON_RELEASE_FOCUS_EVT = "focusAfterReleased";
const FOCUS_TRAP_INJECTION_KEY = Symbol("elFocusTrap");
const isFocusable = (element) => {
if (element.tabIndex > 0 || element.tabIndex === 0 && element.getAttribute("tabIndex") !== null) {
return true;
}
if (element.tabIndex < 0 || element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true") {
return false;
}
switch (element.nodeName) {
case "A": {
return !!element.href && element.rel !== "ignore";
}
case "INPUT": {
return !(element.type === "hidden" || element.type === "file");
}
case "BUTTON":
case "SELECT":
case "TEXTAREA": {
return true;
}
default: {
return false;
}
}
};
const focusReason = ref();
const lastUserFocusTimestamp = ref(0);
const lastAutomatedFocusTimestamp = ref(0);
let focusReasonUserCount = 0;
const obtainAllFocusableElements = (element) => {
const nodes = [];
const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
if (node.disabled || node.hidden || isHiddenInput)
return NodeFilter.FILTER_SKIP;
return node.tabIndex >= 0 || node === document.activeElement ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
while (walker.nextNode())
nodes.push(walker.currentNode);
return nodes;
};
const getVisibleElement = (elements, container) => {
for (const element of elements) {
if (!isHidden(element, container))
return element;
}
};
const isHidden = (element, container) => {
if (process.env.NODE_ENV === "test")
return false;
if (getComputedStyle(element).visibility === "hidden")
return true;
while (element) {
if (container && element === container)
return false;
if (getComputedStyle(element).display === "none")
return true;
element = element.parentElement;
}
return false;
};
const getEdges = (container) => {
const focusable = obtainAllFocusableElements(container);
const first = getVisibleElement(focusable, container);
const last = getVisibleElement(focusable.reverse(), container);
return [first, last];
};
const isSelectable = (element) => {
return element instanceof HTMLInputElement && "select" in element;
};
const tryFocus = (element, shouldSelect) => {
if (element && element.focus) {
const prevFocusedElement = document.activeElement;
let cleanup = false;
if (isElement(element) && !isFocusable(element) && !element.getAttribute("tabindex")) {
element.setAttribute("tabindex", "-1");
cleanup = true;
}
element.focus({ preventScroll: true });
lastAutomatedFocusTimestamp.value = window.performance.now();
if (element !== prevFocusedElement && isSelectable(element) && shouldSelect) {
element.select();
}
if (isElement(element) && cleanup) {
element.removeAttribute("tabindex");
}
}
};
function removeFromStack(list, item) {
const copy = [...list];
const idx = list.indexOf(item);
if (idx !== -1) {
copy.splice(idx, 1);
}
return copy;
}
const createFocusableStack = () => {
let stack = [];
const push = (layer) => {
const currentLayer = stack[0];
if (currentLayer && layer !== currentLayer) {
currentLayer.pause();
}
stack = removeFromStack(stack, layer);
stack.unshift(layer);
};
const remove = (layer) => {
var _a, _b;
stack = removeFromStack(stack, layer);
(_b = (_a = stack[0]) == null ? void 0 : _a.resume) == null ? void 0 : _b.call(_a);
};
return {
push,
remove
};
};
const focusFirstDescendant = (elements, shouldSelect = false) => {
const prevFocusedElement = document.activeElement;
for (const element of elements) {
tryFocus(element, shouldSelect);
if (document.activeElement !== prevFocusedElement)
return;
}
};
const focusableStack = createFocusableStack();
const isFocusCausedByUserEvent = () => {
return lastUserFocusTimestamp.value > lastAutomatedFocusTimestamp.value;
};
const notifyFocusReasonPointer = () => {
focusReason.value = "pointer";
lastUserFocusTimestamp.value = window.performance.now();
};
const notifyFocusReasonKeydown = () => {
focusReason.value = "keyboard";
lastUserFocusTimestamp.value = window.performance.now();
};
const useFocusReason = () => {
onMounted(() => {
if (focusReasonUserCount === 0) {
document.addEventListener("mousedown", notifyFocusReasonPointer);
document.addEventListener("touchstart", notifyFocusReasonPointer);
document.addEventListener("keydown", notifyFocusReasonKeydown);
}
focusReasonUserCount++;
});
onBeforeUnmount(() => {
focusReasonUserCount--;
if (focusReasonUserCount <= 0) {
document.removeEventListener("mousedown", notifyFocusReasonPointer);
document.removeEventListener("touchstart", notifyFocusReasonPointer);
document.removeEventListener("keydown", notifyFocusReasonKeydown);
}
});
return {
focusReason,
lastUserFocusTimestamp,
lastAutomatedFocusTimestamp
};
};
const createFocusOutPreventedEvent = (detail) => {
return new CustomEvent(FOCUSOUT_PREVENTED, {
...FOCUSOUT_PREVENTED_OPTS,
detail
});
};
let registeredEscapeHandlers = [];
const cachedHandler = (event) => {
if (event.code === EVENT_CODE.esc) {
registeredEscapeHandlers.forEach((registeredHandler) => registeredHandler(event));
}
};
const useEscapeKeydown = (handler) => {
onMounted(() => {
if (registeredEscapeHandlers.length === 0) {
document.addEventListener("keydown", cachedHandler);
}
if (isClient)
registeredEscapeHandlers.push(handler);
});
onBeforeUnmount(() => {
registeredEscapeHandlers = registeredEscapeHandlers.filter((registeredHandler) => registeredHandler !== handler);
if (registeredEscapeHandlers.length === 0) {
if (isClient)
document.removeEventListener("keydown", cachedHandler);
}
});
};
const _sfc_main$1 = defineComponent({
name: "ElFocusTrap",
inheritAttrs: false,
props: {
loop: Boolean,
trapped: Boolean,
focusTrapEl: Object,
focusStartEl: {
type: [Object, String],
default: "first"
}
},
emits: [
ON_TRAP_FOCUS_EVT,
ON_RELEASE_FOCUS_EVT,
"focusin",
"focusout",
"focusout-prevented",
"release-requested"
],
setup(props, { emit }) {
const forwardRef = ref();
let lastFocusBeforeTrapped;
let lastFocusAfterTrapped;
const { focusReason: focusReason2 } = useFocusReason();
useEscapeKeydown((event) => {
if (props.trapped && !focusLayer.paused) {
emit("release-requested", event);
}
});
const focusLayer = {
paused: false,
pause() {
this.paused = true;
},
resume() {
this.paused = false;
}
};
const onKeydown = (e) => {
if (!props.loop && !props.trapped)
return;
if (focusLayer.paused)
return;
const { code, altKey, ctrlKey, metaKey, currentTarget, shiftKey } = e;
const { loop } = props;
const isTabbing = code === EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey;
const currentFocusingEl = document.activeElement;
if (isTabbing && currentFocusingEl) {
const container = currentTarget;
const [first, last] = getEdges(container);
const isTabbable = first && last;
if (!isTabbable) {
if (currentFocusingEl === container) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason2.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
}
}
} else {
if (!shiftKey && currentFocusingEl === last) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason2.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
if (loop)
tryFocus(first, true);
}
} else if (shiftKey && [first, container].includes(currentFocusingEl)) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason2.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
if (loop)
tryFocus(last, true);
}
}
}
}
};
provide(FOCUS_TRAP_INJECTION_KEY, {
focusTrapRef: forwardRef,
onKeydown
});
watch(() => props.focusTrapEl, (focusTrapEl) => {
if (focusTrapEl) {
forwardRef.value = focusTrapEl;
}
}, { immediate: true });
watch([forwardRef], ([forwardRef2], [oldForwardRef]) => {
if (forwardRef2) {
forwardRef2.addEventListener("keydown", onKeydown);
forwardRef2.addEventListener("focusin", onFocusIn);
forwardRef2.addEventListener("focusout", onFocusOut);
}
if (oldForwardRef) {
oldForwardRef.removeEventListener("keydown", onKeydown);
oldForwardRef.removeEventListener("focusin", onFocusIn);
oldForwardRef.removeEventListener("focusout", onFocusOut);
}
});
const trapOnFocus = (e) => {
emit(ON_TRAP_FOCUS_EVT, e);
};
const releaseOnFocus = (e) => emit(ON_RELEASE_FOCUS_EVT, e);
const onFocusIn = (e) => {
const trapContainer = unref(forwardRef);
if (!trapContainer)
return;
const target = e.target;
const relatedTarget = e.relatedTarget;
const isFocusedInTrap = target && trapContainer.contains(target);
if (!props.trapped) {
const isPrevFocusedInTrap = relatedTarget && trapContainer.contains(relatedTarget);
if (!isPrevFocusedInTrap) {
lastFocusBeforeTrapped = relatedTarget;
}
}
if (isFocusedInTrap)
emit("focusin", e);
if (focusLayer.paused)
return;
if (props.trapped) {
if (isFocusedInTrap) {
lastFocusAfterTrapped = target;
} else {
tryFocus(lastFocusAfterTrapped, true);
}
}
};
const onFocusOut = (e) => {
const trapContainer = unref(forwardRef);
if (focusLayer.paused || !trapContainer)
return;
if (props.trapped) {
const relatedTarget = e.relatedTarget;
if (!isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) {
setTimeout(() => {
if (!focusLayer.paused && props.trapped) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason2.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
tryFocus(lastFocusAfterTrapped, true);
}
}
}, 0);
}
} else {
const target = e.target;
const isFocusedInTrap = target && trapContainer.contains(target);
if (!isFocusedInTrap)
emit("focusout", e);
}
};
async function startTrap() {
await nextTick();
const trapContainer = unref(forwardRef);
if (trapContainer) {
focusableStack.push(focusLayer);
const prevFocusedElement = trapContainer.contains(document.activeElement) ? lastFocusBeforeTrapped : document.activeElement;
lastFocusBeforeTrapped = prevFocusedElement;
const isPrevFocusContained = trapContainer.contains(prevFocusedElement);
if (!isPrevFocusContained) {
const focusEvent = new Event(FOCUS_AFTER_TRAPPED, FOCUS_AFTER_TRAPPED_OPTS);
trapContainer.addEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus);
trapContainer.dispatchEvent(focusEvent);
if (!focusEvent.defaultPrevented) {
nextTick(() => {
let focusStartEl = props.focusStartEl;
if (!isString(focusStartEl)) {
tryFocus(focusStartEl);
if (document.activeElement !== focusStartEl) {
focusStartEl = "first";
}
}
if (focusStartEl === "first") {
focusFirstDescendant(obtainAllFocusableElements(trapContainer), true);
}
if (document.activeElement === prevFocusedElement || focusStartEl === "container") {
tryFocus(trapContainer);
}
});
}
}
}
}
function stopTrap() {
const trapContainer = unref(forwardRef);
if (trapContainer) {
trapContainer.removeEventListener(FOCUS_AFTER_TRAPPED, trapOnFocus);
const releasedEvent = new CustomEvent(FOCUS_AFTER_RELEASED, {
...FOCUS_AFTER_TRAPPED_OPTS,
detail: {
focusReason: focusReason2.value
}
});
trapContainer.addEventListener(FOCUS_AFTER_RELEASED, releaseOnFocus);
trapContainer.dispatchEvent(releasedEvent);
if (!releasedEvent.defaultPrevented && (focusReason2.value == "keyboard" || !isFocusCausedByUserEvent() || trapContainer.contains(document.activeElement))) {
tryFocus(lastFocusBeforeTrapped != null ? lastFocusBeforeTrapped : document.body);
}
trapContainer.removeEventListener(FOCUS_AFTER_RELEASED, releaseOnFocus);
focusableStack.remove(focusLayer);
}
}
onMounted(() => {
if (props.trapped) {
startTrap();
}
watch(() => props.trapped, (trapped) => {
if (trapped) {
startTrap();
} else {
stopTrap();
}
});
});
onBeforeUnmount(() => {
if (props.trapped) {
stopTrap();
}
if (forwardRef.value) {
forwardRef.value.removeEventListener("keydown", onKeydown);
forwardRef.value.removeEventListener("focusin", onFocusIn);
forwardRef.value.removeEventListener("focusout", onFocusOut);
forwardRef.value = void 0;
}
});
return {
onKeydown
};
}
});
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return renderSlot(_ctx.$slots, "default", { handleKeydown: _ctx.onKeydown });
}
var ElFocusTrap = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render], ["__file", "focus-trap.vue"]]);
const teleportProps = buildProps({
to: {
type: definePropType([String, Object]),
required: true
},
disabled: Boolean
});
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "teleport",
props: teleportProps,
setup(__props) {
return (_ctx, _cache) => {
return _ctx.disabled ? renderSlot(_ctx.$slots, "default", { key: 0 }) : (openBlock(), createBlock(Teleport$1, {
key: 1,
to: _ctx.to
}, [
renderSlot(_ctx.$slots, "default")
], 8, ["to"]));
};
}
});
var Teleport = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "teleport.vue"]]);
const ElTeleport = withInstall(Teleport);
export {
ElTeleport as E,
FOCUS_TRAP_INJECTION_KEY as F,
ElFocusTrap as a,
isFocusable as i,
tryFocus as t
};
//# sourceMappingURL=index-D-ahPibB.js.map