fix: update tree title render & switchIcon

pull/4577/head
tangjinzhou 2021-08-25 21:15:39 +08:00
parent eff286d0e5
commit e8b1f68899
12 changed files with 124 additions and 80 deletions

View File

@ -162,6 +162,7 @@ export { default as Table, TableColumn, TableColumnGroup } from './table';
export type { TransferProps } from './transfer'; export type { TransferProps } from './transfer';
export { default as Transfer } from './transfer'; export { default as Transfer } from './transfer';
export type { TreeProps, DirectoryTreeProps } from './tree';
export { default as Tree, TreeNode, DirectoryTree } from './tree'; export { default as Tree, TreeNode, DirectoryTree } from './tree';
export type { TreeSelectProps } from './tree-select'; export type { TreeSelectProps } from './tree-select';

View File

@ -7,7 +7,7 @@ import VcTree, { TreeNode } from '../vc-tree';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { filterEmpty } from '../_util/props-util'; import { filterEmpty } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps'; import initDefaultProps from '../_util/props-util/initDefaultProps';
import type { DataNode, DragNodeEvent, FieldNames, Key } from '../vc-tree/interface'; import type { DataNode, EventDataNode, FieldNames, Key } from '../vc-tree/interface';
import type { TreeNodeProps } from '../vc-tree/props'; import type { TreeNodeProps } from '../vc-tree/props';
import { treeProps as vcTreeProps } from '../vc-tree/props'; import { treeProps as vcTreeProps } from '../vc-tree/props';
import useConfigInject from '../_util/hooks/useConfigInject'; import useConfigInject from '../_util/hooks/useConfigInject';
@ -42,7 +42,7 @@ export type AntTreeNodeProps = TreeNodeProps;
export type TreeDataItem = DataNode; export type TreeDataItem = DataNode;
export interface AntTreeNodeBaseEvent { export interface AntTreeNodeBaseEvent {
node: DataNode; node: EventDataNode;
nativeEvent: MouseEvent; nativeEvent: MouseEvent;
} }
@ -63,7 +63,7 @@ export interface AntTreeNodeExpandedEvent extends AntTreeNodeBaseEvent {
} }
export interface AntTreeNodeMouseEvent { export interface AntTreeNodeMouseEvent {
node: DataNode; node: EventDataNode;
event: DragEvent; event: DragEvent;
} }
@ -72,8 +72,8 @@ export interface AntTreeNodeDragEnterEvent extends AntTreeNodeMouseEvent {
} }
export interface AntTreeNodeDropEvent { export interface AntTreeNodeDropEvent {
node: DragNodeEvent; node: EventDataNode;
dragNode: DragNodeEvent; dragNode: EventDataNode;
dragNodesKeys: Key[]; dragNodesKeys: Key[];
dropPosition: number; dropPosition: number;
dropToGap?: boolean; dropToGap?: boolean;
@ -192,7 +192,7 @@ export default defineComponent({
const { const {
showIcon, showIcon,
showLine, showLine,
switcherIcon = slots.switcherIcon?.(), switcherIcon = slots.switcherIcon,
icon = slots.icon, icon = slots.icon,
blockNode, blockNode,
checkable, checkable,

View File

@ -2,19 +2,24 @@ import type { App, Plugin } from 'vue';
import Tree from './Tree'; import Tree from './Tree';
import DirectoryTree from './DirectoryTree'; import DirectoryTree from './DirectoryTree';
export { EventDataNode, DataNode } from '../vc-tree/interface'; export type { EventDataNode, DataNode } from '../vc-tree/interface';
export { export type {
TreeProps, TreeProps,
AntTreeNodeMouseEvent, AntTreeNodeMouseEvent,
AntTreeNodeExpandedEvent, AntTreeNodeExpandedEvent,
AntTreeNodeCheckedEvent, AntTreeNodeCheckedEvent,
AntTreeNodeSelectedEvent, AntTreeNodeSelectedEvent,
AntTreeNodeDragEnterEvent,
AntTreeNodeDropEvent,
AntdTreeNodeAttribute, AntdTreeNodeAttribute,
TreeDataItem, TreeDataItem,
} from './Tree'; } from './Tree';
export { ExpandAction as DirectoryTreeExpandAction, DirectoryTreeProps } from './DirectoryTree'; export type {
ExpandAction as DirectoryTreeExpandAction,
DirectoryTreeProps,
} from './DirectoryTree';
Tree.TreeNode.name = 'ATreeNode'; Tree.TreeNode.name = 'ATreeNode';
Tree.DirectoryTree = DirectoryTree; Tree.DirectoryTree = DirectoryTree;

View File

@ -12,41 +12,46 @@ export default function renderSwitcherIcon(
prefixCls: string, prefixCls: string,
switcherIcon: any, switcherIcon: any,
showLine: boolean | { showLeafIcon: boolean } | undefined, showLine: boolean | { showLeafIcon: boolean } | undefined,
{ isLeaf, expanded, loading }: AntTreeNodeProps, props: AntTreeNodeProps,
) { ) {
const { isLeaf, expanded, loading } = props;
let icon = switcherIcon;
if (loading) { if (loading) {
return <LoadingOutlined class={`${prefixCls}-switcher-loading-icon`} />; return <LoadingOutlined class={`${prefixCls}-switcher-loading-icon`} />;
} }
let showLeafIcon; let showLeafIcon: boolean;
if (showLine && typeof showLine === 'object') { if (showLine && typeof showLine === 'object') {
showLeafIcon = showLine.showLeafIcon; showLeafIcon = showLine.showLeafIcon;
} }
let defaultIcon = null;
const switcherCls = `${prefixCls}-switcher-icon`;
if (isLeaf) { if (isLeaf) {
if (showLine) { if (showLine) {
if (typeof showLine === 'object' && !showLeafIcon) { if (typeof showLine === 'object' && !showLeafIcon) {
return <span class={`${prefixCls}-switcher-leaf-line`} />; defaultIcon = <span class={`${prefixCls}-switcher-leaf-line`} />;
} else {
defaultIcon = <FileOutlined class={`${prefixCls}-switcher-line-icon`} />;
} }
return <FileOutlined class={`${prefixCls}-switcher-line-icon`} />;
} }
return null; return defaultIcon;
} else {
defaultIcon = <CaretDownFilled class={switcherCls} />;
if (showLine) {
defaultIcon = expanded ? (
<MinusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
) : (
<PlusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
);
}
} }
const switcherCls = `${prefixCls}-switcher-icon`;
if (isValidElement(switcherIcon)) { if (typeof switcherIcon === 'function') {
return cloneVNode(switcherIcon, { icon = switcherIcon({ ...props, defaultIcon, switcherCls });
} else if (isValidElement(icon)) {
icon = cloneVNode(icon, {
class: switcherCls, class: switcherCls,
}); });
} }
if (switcherIcon) { return icon || defaultIcon;
return switcherIcon;
}
if (showLine) {
return expanded ? (
<MinusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
) : (
<PlusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
);
}
return <CaretDownFilled class={switcherCls} />;
} }

View File

@ -101,15 +101,16 @@ const SingleSelector = defineComponent<SelectorProps>({
// custom tree-select title by slot // custom tree-select title by slot
if (item && treeSelectContext.value.slots) { if (item && treeSelectContext.value.slots) {
titleNode = titleNode =
item.label ||
treeSelectContext.value.slots[item?.option?.data?.slots?.title] || treeSelectContext.value.slots[item?.option?.data?.slots?.title] ||
treeSelectContext.value.slots.title; treeSelectContext.value.slots.title ||
item.label;
if (typeof titleNode === 'function') { if (typeof titleNode === 'function') {
titleNode = titleNode(item.option?.data || {}); titleNode = titleNode(item.option?.data || {});
} else if (treeSelectContext.value.slots.titleRender) {
// title titleRender title titleRender
titleNode = treeSelectContext.value.slots.titleRender(item.option?.data || {});
} }
// else if (treeSelectContext.value.slots.titleRender) {
// // title titleRender title titleRender
// titleNode = treeSelectContext.value.slots.titleRender(item.option?.data || {});
// }
} else { } else {
titleNode = item?.label; titleNode = item?.label;
} }

View File

@ -1,3 +1,4 @@
import { filterEmpty } from '../../_util/props-util';
import type { VNodeChild } from 'vue'; import type { VNodeChild } from 'vue';
import { camelize } from 'vue'; import { camelize } from 'vue';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
@ -16,7 +17,7 @@ function isTreeSelectNode(node: any) {
} }
export function convertChildrenToData(rootNodes: VNodeChild): DataNode[] { export function convertChildrenToData(rootNodes: VNodeChild): DataNode[] {
function dig(treeNodes: any[] = []): DataNode[] { function dig(treeNodes: any[] = []): DataNode[] {
return treeNodes.map(treeNode => { return filterEmpty(treeNodes).map(treeNode => {
// Filter invalidate node // Filter invalidate node
if (!isTreeSelectNode(treeNode)) { if (!isTreeSelectNode(treeNode)) {
warning(!treeNode, 'TreeSelect/TreeSelectNode can only accept TreeSelectNode as children.'); warning(!treeNode, 'TreeSelect/TreeSelectNode can only accept TreeSelectNode as children.');

View File

@ -245,7 +245,7 @@ export default defineComponent({
cleanDragState(); cleanDragState();
if (onDragend && !outsideTree) { if (onDragend && !outsideTree) {
onDragend({ event, node: node.eventData.value }); onDragend({ event, node: node.eventData });
} }
dragNode = null; dragNode = null;
@ -267,17 +267,17 @@ export default defineComponent({
y: event.clientY, y: event.clientY,
}; };
const newExpandedKeys = arrDel(expandedKeys.value, eventKey.value); const newExpandedKeys = arrDel(expandedKeys.value, eventKey);
dragState.dragging = true; dragState.dragging = true;
dragState.dragChildrenKeys = getDragChildrenKeys(eventKey.value, keyEntities.value); dragState.dragChildrenKeys = getDragChildrenKeys(eventKey, keyEntities.value);
indent.value = listRef.value.getIndentWidth(); indent.value = listRef.value.getIndentWidth();
setExpandedKeys(newExpandedKeys); setExpandedKeys(newExpandedKeys);
window.addEventListener('dragend', onWindowDragEnd); window.addEventListener('dragend', onWindowDragEnd);
if (onDragstart) { if (onDragstart) {
onDragstart({ event, node: eventData.value }); onDragstart({ event, node: eventData });
} }
}; };
@ -339,25 +339,25 @@ export default defineComponent({
clearTimeout(delayedDragEnterLogic[key]); clearTimeout(delayedDragEnterLogic[key]);
}); });
if (dragNode.eventKey.value !== node.eventKey.value) { if (dragNode.eventKey !== node.eventKey) {
// hoist expand logic here // hoist expand logic here
// since if logic is on the bottom // since if logic is on the bottom
// it will be blocked by abstract dragover node check // it will be blocked by abstract dragover node check
// => if you dragenter from top, you mouse will still be consider as in the top node // => if you dragenter from top, you mouse will still be consider as in the top node
delayedDragEnterLogic[node.pos.value] = window.setTimeout(() => { delayedDragEnterLogic[node.pos] = window.setTimeout(() => {
if (!dragState.dragging) return; if (!dragState.dragging) return;
let newExpandedKeys = [...expandedKeys.value]; let newExpandedKeys = [...expandedKeys.value];
const entity = keyEntities.value[node.eventKey.value]; const entity = keyEntities.value[node.eventKey];
if (entity && (entity.children || []).length) { if (entity && (entity.children || []).length) {
newExpandedKeys = arrAdd(expandedKeys.value, node.eventKey.value); newExpandedKeys = arrAdd(expandedKeys.value, node.eventKey);
} }
setExpandedKeys(newExpandedKeys); setExpandedKeys(newExpandedKeys);
if (onExpand) { if (onExpand) {
onExpand(newExpandedKeys, { onExpand(newExpandedKeys, {
node: node.eventData.value, node: node.eventData,
expanded: true, expanded: true,
nativeEvent: event, nativeEvent: event,
}); });
@ -366,7 +366,7 @@ export default defineComponent({
} }
// Skip if drag node is self // Skip if drag node is self
if (dragNode.eventKey.value === dropTargetKey && dropLevelOffset === 0) { if (dragNode.eventKey === dropTargetKey && dropLevelOffset === 0) {
Object.assign(dragState, { Object.assign(dragState, {
dragOverNodeKey: null, dragOverNodeKey: null,
dropPosition: null, dropPosition: null,
@ -393,7 +393,7 @@ export default defineComponent({
if (onDragenter) { if (onDragenter) {
onDragenter({ onDragenter({
event, event,
node: node.eventData.value, node: node.eventData,
expandedKeys: expandedKeys.value, expandedKeys: expandedKeys.value,
}); });
} }
@ -431,7 +431,7 @@ export default defineComponent({
// Update drag position // Update drag position
if (dragNode.eventKey.value === dropTargetKey && dropLevelOffset === 0) { if (dragNode.eventKey === dropTargetKey && dropLevelOffset === 0) {
if ( if (
!( !(
dragState.dropPosition === null && dragState.dropPosition === null &&
@ -476,7 +476,7 @@ export default defineComponent({
} }
if (onDragover) { if (onDragover) {
onDragover({ event, node: node.eventData.value }); onDragover({ event, node: node.eventData });
} }
}; };
@ -484,7 +484,7 @@ export default defineComponent({
const { onDragleave } = props; const { onDragleave } = props;
if (onDragleave) { if (onDragleave) {
onDragleave({ event, node: node.eventData.value }); onDragleave({ event, node: node.eventData });
} }
}; };
const onNodeDrop = (event: MouseEvent, _node, outsideTree = false) => { const onNodeDrop = (event: MouseEvent, _node, outsideTree = false) => {
@ -517,8 +517,8 @@ export default defineComponent({
const dropResult = { const dropResult = {
event, event,
node: convertNodePropsToEventData(abstractDropNodeProps), node: convertNodePropsToEventData(abstractDropNodeProps),
dragNode: dragNode ? dragNode.eventData.value : null, dragNode: dragNode ? dragNode.eventData : null,
dragNodesKeys: [dragNode.eventKey.value].concat(dragChildrenKeys), dragNodesKeys: [dragNode.eventKey].concat(dragChildrenKeys),
dropToGap: dropPosition !== 0, dropToGap: dropPosition !== 0,
dropPosition: dropPosition + Number(posArr[posArr.length - 1]), dropPosition: dropPosition + Number(posArr[posArr.length - 1]),
}; };

View File

@ -2,11 +2,20 @@ import { useInjectTreeContext } from './contextTypes';
import { getDataAndAria } from './util'; import { getDataAndAria } from './util';
import Indent from './Indent'; import Indent from './Indent';
import { convertNodePropsToEventData } from './utils/treeUtil'; import { convertNodePropsToEventData } from './utils/treeUtil';
import { computed, defineComponent, onMounted, onUpdated, ref } from 'vue'; import {
computed,
defineComponent,
getCurrentInstance,
onMounted,
onUpdated,
reactive,
ref,
} from 'vue';
import { treeNodeProps } from './props'; import { treeNodeProps } from './props';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { warning } from '../vc-util/warning'; import { warning } from '../vc-util/warning';
import type { DragNodeEvent } from './interface'; import type { DragNodeEvent, Key } from './interface';
import pick from 'lodash-es/pick';
const ICON_OPEN = 'open'; const ICON_OPEN = 'open';
const ICON_CLOSE = 'close'; const ICON_CLOSE = 'close';
@ -87,16 +96,36 @@ export default defineComponent({
return treeSelectable; return treeSelectable;
}); });
const renderArgsData = computed(() => {
return {
...pick(props, [
'active',
'checkable',
'checked',
'disableCheckbox',
'disabled',
'expanded',
'isLeaf',
'loading',
'selectable',
'selected',
'halfChecked',
]),
...props.data,
dataRef: props.data,
isLeaf: isLeaf.value,
};
});
const eventData = computed(() => { const eventData = computed(() => {
return convertNodePropsToEventData(props); return convertNodePropsToEventData(props);
}); });
const dragNodeEvent: DragNodeEvent = { const dragNodeEvent: DragNodeEvent = reactive({
eventData, eventData,
eventKey: computed(() => props.eventKey), eventKey: computed(() => props.eventKey),
selectHandle, selectHandle,
pos: computed(() => props.pos), pos: computed(() => props.pos),
}; key: getCurrentInstance().vnode.key as Key,
});
expose(dragNodeEvent); expose(dragNodeEvent);
const onSelectorDoubleClick = (e: MouseEvent) => { const onSelectorDoubleClick = (e: MouseEvent) => {
const { onNodeDoubleClick } = context.value; const { onNodeDoubleClick } = context.value;
@ -214,7 +243,7 @@ export default defineComponent({
onNodeExpand(e, eventData.value); onNodeExpand(e, eventData.value);
}; };
const renderSwitcherIconDom = (isLeaf: boolean) => { const renderSwitcherIconDom = () => {
const { const {
switcherIcon: switcherIconFromProps = slots.switcherIcon || switcherIcon: switcherIconFromProps = slots.switcherIcon ||
context.value.slots?.[props.data?.slots?.switcherIcon], context.value.slots?.[props.data?.slots?.switcherIcon],
@ -224,7 +253,7 @@ export default defineComponent({
const switcherIcon = switcherIconFromProps || switcherIconFromCtx; const switcherIcon = switcherIconFromProps || switcherIconFromCtx;
// if switcherIconDom is null, no render switcher span // if switcherIconDom is null, no render switcher span
if (typeof switcherIcon === 'function') { if (typeof switcherIcon === 'function') {
return switcherIcon({ ...props, isLeaf }); return switcherIcon(renderArgsData.value);
} }
return switcherIcon; return switcherIcon;
}; };
@ -259,11 +288,9 @@ export default defineComponent({
const renderSwitcher = () => { const renderSwitcher = () => {
const { expanded } = props; const { expanded } = props;
const { prefixCls } = context.value; const { prefixCls } = context.value;
// if switcherIconDom is null, no render switcher span
const switcherIconDom = renderSwitcherIconDom();
if (isLeaf.value) { if (isLeaf.value) {
// if switcherIconDom is null, no render switcher span
const switcherIconDom = renderSwitcherIconDom(true);
return switcherIconDom !== false ? ( return switcherIconDom !== false ? (
<span class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}> <span class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}>
{switcherIconDom} {switcherIconDom}
@ -276,8 +303,6 @@ export default defineComponent({
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`, `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
); );
const switcherIconDom = renderSwitcherIconDom(false);
return switcherIconDom !== false ? ( return switcherIconDom !== false ? (
<span onClick={onExpand} class={switcherCls}> <span onClick={onExpand} class={switcherCls}>
{switcherIconDom} {switcherIconDom}
@ -348,14 +373,19 @@ export default defineComponent({
// Icon + Title // Icon + Title
const renderSelector = () => { const renderSelector = () => {
const { const {
title = slots.title || // title = slots.title ||
context.value.slots?.[props.data?.slots?.title] || // context.value.slots?.[props.data?.slots?.title] ||
context.value.slots?.title, // context.value.slots?.title,
selected, selected,
icon = slots.icon, icon = slots.icon,
loading, loading,
data, data,
} = props; } = props;
const title =
slots.title ||
context.value.slots?.[props.data?.slots?.title] ||
context.value.slots?.title ||
props.title;
const { const {
prefixCls, prefixCls,
showIcon, showIcon,
@ -377,7 +407,7 @@ export default defineComponent({
$icon = currentIcon ? ( $icon = currentIcon ? (
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}> <span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
{typeof currentIcon === 'function' ? currentIcon(props) : currentIcon} {typeof currentIcon === 'function' ? currentIcon(renderArgsData.value) : currentIcon}
</span> </span>
) : ( ) : (
renderIcon() renderIcon()
@ -389,9 +419,9 @@ export default defineComponent({
// Title // Title
let titleNode: any; let titleNode: any;
if (typeof title === 'function') { if (typeof title === 'function') {
titleNode = title(data); titleNode = title(renderArgsData.value);
} else if (contextSlots.titleRender) { } else if (contextSlots.titleRender) {
titleNode = contextSlots.titleRender(data); titleNode = contextSlots.titleRender(renderArgsData.value);
} else { } else {
titleNode = title; titleNode = title;
} }

View File

@ -1,4 +1,4 @@
import type { ComputedRef, CSSProperties, Ref, VNode } from 'vue'; import type { CSSProperties, VNode } from 'vue';
import type { TreeNodeProps } from './props'; import type { TreeNodeProps } from './props';
export type { ScrollTo } from '../vc-virtual-list/List'; export type { ScrollTo } from '../vc-virtual-list/List';
@ -43,10 +43,11 @@ export type Key = string | number;
export type NodeElement = VNode<TreeNodeProps>; export type NodeElement = VNode<TreeNodeProps>;
export type DragNodeEvent = { export type DragNodeEvent = {
eventData: ComputedRef<EventDataNode>; key: Key;
eventKey: ComputedRef<Key>; eventData: EventDataNode;
selectHandle: Ref<HTMLSpanElement>; eventKey: Key;
pos: ComputedRef<string>; selectHandle: HTMLSpanElement;
pos: string;
}; };
export interface Entity { export interface Entity {
node: NodeElement; node: NodeElement;

View File

@ -108,7 +108,7 @@ export function calcDropPosition(
const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent; const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent;
// find abstract drop node by horizontal offset // find abstract drop node by horizontal offset
let abstractDropNodeEntity: DataEntity = keyEntities[targetNode.eventKey.value]; let abstractDropNodeEntity: DataEntity = keyEntities[targetNode.eventKey];
if (clientY < top + height / 2) { if (clientY < top + height / 2) {
// first half, set abstract drop node to previous node // first half, set abstract drop node to previous node
@ -150,7 +150,7 @@ export function calcDropPosition(
dropNode: abstractDropDataNode, dropNode: abstractDropDataNode,
dropPosition: -1, dropPosition: -1,
}) && }) &&
abstractDropNodeEntity.key === targetNode.eventKey.value abstractDropNodeEntity.key === targetNode.eventKey
) { ) {
// first half of first node in first level // first half of first node in first level
dropPosition = -1; dropPosition = -1;

View File

@ -14,6 +14,7 @@ import Omit from 'omit.js';
import type { VNodeChild } from 'vue'; import type { VNodeChild } from 'vue';
import { camelize } from 'vue'; import { camelize } from 'vue';
import type { TreeNodeProps } from '../props'; import type { TreeNodeProps } from '../props';
import { filterEmpty } from '../../_util/props-util';
export function getKey(key: Key, pos: string) { export function getKey(key: Key, pos: string) {
if (key !== null && key !== undefined) { if (key !== null && key !== undefined) {
@ -66,7 +67,7 @@ export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames)
*/ */
export function convertTreeToData(rootNodes: VNodeChild): DataNode[] { export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
function dig(node: VNodeChild = []): DataNode[] { function dig(node: VNodeChild = []): DataNode[] {
const treeNodes = node as NodeElement[]; const treeNodes = filterEmpty(node as NodeElement[]);
return treeNodes.map(treeNode => { return treeNodes.map(treeNode => {
// Filter invalidate node // Filter invalidate node
if (!isTreeNode(treeNode)) { if (!isTreeNode(treeNode)) {
@ -114,7 +115,6 @@ export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
return dataNode; return dataNode;
}); });
} }
return dig(rootNodes); return dig(rootNodes);
} }

2
v2-doc

@ -1 +1 @@
Subproject commit 3f94a02d16bac2377d71e26948ffe45571dae49e Subproject commit bf82708b6f320f1e9b185c6eba58b4ce08593fc3