Angular Material 18+ 高级教程 – CDK Drag and Drop

前言

CDK Drag and Drop 和 CDK Scrolling 都是在 Angular Material v7 中推出的。

它们有一个巧妙的共同点,那就是与 Material Design 没有什么关联。

这也导致了它和 CDK Scrolling 一样,功能不完善,只能满足非常简单的开发需求。

我们之前说过,CDK 是 Angular Material 团队在开发 Angular Material 时顺手抽象出来的。

它们的关系是 CDK 服务于 Angular Material,而 Angular Material 服务于 Google Products。

因此,如果一个 CDK 功能没有被 Angular Material 或 Google Products 重用,那么这个功能注定不会得到关注,不会完善,也不会好用 (不管社区多么努力 vote)。

本篇,让我们一起看看这个不太好用的 CDK Drag and Drop 吧🚀

 

参考

Docs – CDK Drag and Drop

Github Issue – Drag Drop Sortable, mixed orientation support

 

Free Drag

这是一个 box

<div class="box">Drag me around</div>

Styles

.box {
  width: 192px;
  height: 192px;
  background-color: pink;
  color: blue;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 20px;
  border: 4px solid red;
  cursor: grab;
}
View Code

效果

我们只要加上一个 CdkDrag 指令,它就是能 drag 移位了。

<div class="box" cdkDrag>Drag me around</div>

效果

The principle behind

Drag 的实现原理非常简单,就是监听 mouse down/move/up 然后添加 transform translate 到 element,让它顺着 mouse position 移位。

也因为这样,当 element 被 drag 以后,它不会影响原来的布局。

我们换一个例子展现这个特性

<div class="item-list">
  <div class="item">Alex</div>
  <div class="item" cdkDrag>David</div>
  <div class="item">Jay</div>
  <div class="item">Stefanie</div>
</div>
View Code
.item-list {
  border: 1px solid black;
  padding: 16px;
  width: 256px;
  display: flex;
  flex-direction: column;
  gap: 16px;

  .item {
    padding: 16px;
    border: 1px solid black;
    background-color: white;

    &[cdkDrag] {
      cursor: grab;
    }
  }
}
View Code

David 被拉出来后,Jay 并不会往上移动。

也因为它使用 translate,假如 item-list 有 overflow hidden,当 item 超出范围时会被 hide 掉。

.item-list {
  overflow: hidden;
}

效果

Free drag options

drag 有一些简单的小配置可以玩。

  1. lock axis

    <div class="box" cdkDrag cdkDragLockAxis="x">Drag me around</div>

    加一个 @Input cdkDragLockAxis,它的功效是锁住一个方向 (x or y 轴)。

    lock axis x 意思是只能 drag 只会影响 translate x,translate y 不会改变。

    反过来,lock axis y 就是只改变 translate y,translate x 不会改变。

  2. boundary 边界

    我们包一个 container

    <div class="container">
      <div class="box" cdkDrag>Drag me around</div>
    </div>

    Styles

    .container {
      width: 360px;
      height: 360px;
      border: 1px solid black;
    
      .box {
        // 省略...
      }
    }
    效果

    目前 box 可以 drag 超出 container 范围。

    我们添加 @Input cdkDragBoundary

    <div class="box" cdkDrag cdkDragBoundary=".container">Drag me around</div>

    value 是一个 ancestor element selector,任何 ancestor element 都可以作为边界。

    效果

  3. drag handle

    <div class="box" cdkDrag>
      <span cdkDragHandle>Drag me around</span>
    </div>

    在 drag element 内添加一个 CdkDragHandle 指令,只有精准点击到这个 CdkDragHandle element 才能发起 drag。

    .box {
      // 省略...
    
      [cdkDragHandle] {
        background-color: blue;
        color: white;
      }
    }

    效果

    粉红区域是 drag 不到的,只有中间的蓝色区域可以 drag。

这些 options 都只是一些微不足道的小功能而已,它们都建立在 drag 的基础 (监听 mouse move 设置 translate) 之上。

 

Drag and Drop

单单使用 drag 功能是比较少见的,更多的情况我们是 drag and drop 一起使用。

Simple case

组件

export class TestMyDragComponent {
  readonly names = signal(['Alex', 'David', 'Jay', 'Stefanie']);
}

Template

<div class="item-list">
  @for (name of names(); track name) {
    <div class="item">{{ name }}</div>
  }
</div>

Styles

.item-list {
  border: 1px solid black;
  padding: 16px;
  width: 256px;
  display: flex;
  flex-direction: column;
  gap: 16px;

  .item {
    padding: 16px;
    border: 1px solid black;
    background-color: pink;
  }
}

效果

需求是 drag item 换位置。

每一个 item 都可以 drag 换位置,所以第一步是在 item element 添加 CdkDrag 指令

<div class="item" cdkDrag>{{ name }}</div>

效果

虽然可以 drag 可以换位置,但是排版乱七八糟。

我们需要添加 CdkDropList 指令到 item-list element 上

<div class="item-list" cdkDropList>

效果

仔细看,它其实已经有一些换位效果了,但依然有 2 个大问题:

  1. drag 的时候 item 的 styles 没了

  2. drop 的时候 item 的位置又跳回去了

The principle behind

要解决上述两个问题,我们先把 drap and drop 背后原理搞清楚。

当 CdkDrag 指令自己一个人时,它的任务很简单,就只是 set item translate。

但当它遇上 CdkDropList 指令后,它的任务就不同了。

当 mouse down + mouse move 以后,CdkDrag 会创建出 2 个 elements。

第一个 element 叫 placeholder,by default 它就是 clone from item element。

第二个 element 叫 preview,by default 它也是 clone from item element。

注:placeholder 和 preview 的设计是可以 customize 的,有兴趣的读友可以看官网的例子,太浅我就不教了。

placeholder 会被插入到原本 item 的位置上

原本的 item 会被 cut and paste 到 body,并且定位到千里之外,让它看不见。

preview 也会被 append 到 body 然后 position fixed + translate 定位到 mouse cursor 的位置。

Solve the style issue

好,了解了它的结构,我们的 styles issue 就有眉目了。

preview element 虽然是 clone from item element,但由于它被 append 到 body 了,所以我们之前定义的 CSS selector 就 select 不到它了。

我们把 .item selector 搬出来

效果

preview 的 styles 和 item styles 一模一样了。

Solve the drop jump back issue

dragging 时,item 的顺序已经发生了变化,但在 drop 下去的那一瞬间又都跳回了原位。

这背后的原理其实很简单。

在 dragging (mouse move) 的时候,CdkDrag 和 CdkDropList 会携手合作,查看 drop list 内所有的 drag item 的 bounding client rect,

每当 mouse 进入到 item 里,CdkDropList 就会换 item element 的位置。

注:它换位的方式是透过 set translate 而不是改变 element 在 DOM 里的结构哦。用 translate 的好处是可以 set animation,改变 DOM 结构搞不了 animation。

这个换位就是纯粹的 DOM manipulation 而已,而当 drop 下去时,之前换位 set 的 translate style 通通会被洗掉,于是它就跳回原位了。

要解决这个问题,或者说正确的做法应该是,监听 drop 事件,然后修改 view model items 的顺序,然后通过 @for render 让它渲染出正确的位置。

<div class="item-list" cdkDropList (cdkDropListDropped)="drop($event)">

添加 @Output cdkDropListDropped 和一个 drop 方法

drop(event: CdkDragDrop<unknown>) {
  const clonedNames = [...this.names()];
  moveItemInArray(clonedNames, event.previousIndex, event.currentIndex);
  this.names.set(clonedNames);
}

moveItemInArray 是 CDK Drap and Drop built-in 的函数,它的作用是搭配 CdkDragDrop event 对 array items 做换位,这样我们就不需要自己写换位的 formula 了👍。

注:moveItemInArray 不是 immutable 设计流派的,它会直接 mutate array,虽然有人提议 Angular 团队推出 immutable 版本,但很遗憾,Angular 团队没有意识到它的重要性。

效果

More styling

效果是有了,但是体验还不够好。

我们知道 CDK 是不负责设计的,它只负责 add class。

那就让我们透过这些 class 来美化一下呗。

.item-list {
  border: 1px solid black;
  padding: 16px;
  width: 256px;
  display: flex;
  flex-direction: column;
  gap: 16px;

  .cdk-drag-placeholder {
    opacity: 0; // 隐藏 placeholder
  }

  &.cdk-drop-list-dragging .item:not(.cdk-drag-placeholder) {
    transition: transform 0.25s; // dragging 换位时的 animation
  }
}

.item {
  padding: 16px;
  border: 1px solid black;
  background-color: pink;

  &[cdkDrag] {
    cursor: grab;
  }

  &.cdk-drag-preview.cdk-drag-animating {
    transition: transform 0.25s; // drop 时的 animation
  }
}

最终效果

Focus & Blur 的问题

drag 以后,focus 就没了,drop 以后也不会还原,挺瞎的...😑

一开始是 .cdk-keyboard-focused 和 :focus 两个都有。

开始 drag 以后,.cdk-keyboard-focused 还在,:focus 没了。

drop 以后连 .cdk-keyboard-focused 也没了。

原理很简单,上面讲过了,drag 的时候会搞 placeholder,preview,还会把原本的 item 丢到 body 千里之外。

这一顿操作下来,focus 肯定丢失了。

如果我们知道 focsued element 是哪个 (通常就是当前 drag 的 item 或者它里面的 drag handle),那可以监听 cdkDragEnded  事件然后手动 focus 回去。

 

Drag and drop by keyboard

Angular Material 没有 built-in 支持 drag and drop by keyboard。

相关 Github Issue – Proposal for Keyboard and Screenreader Accessibility

这里提供一个简单,勉强可用的方案。(注:不是完整通用的方案哦)

我们用回上面的例子。

首先要让 item 变成 tabbable,加一个 tabindex 属性给它

再给它一个 focus styles 明显一点

效果

当用户 keydown Alt + ArrowUp, Down, Left, Right 就代表移位。

创建一个 KeyboardDrag 指令

ng g d keyboard-drag

KeyboardDrag 指令

@Directive({
  // 1. 和 cdkDrag 用相同的 selector
  selector: '[cdkDrag]',
  standalone: true
})
export class KeyboardDragDirective {

  // 2. disabled 就不能 keyboard 操作
  readonly disabled = input(false, { alias: 'cdkDragDisabled' });
  
  constructor() {
    const cdkDropList = inject(CdkDropList, { optional: true });
    // 3. 我们只支持 1 个场景,那就是在一个 DropList 里面 drag 移位
    //    没有 DropList 表示是 free drag 场景,和我们无关,直接 return skip 掉。
    if (cdkDropList === null) return; 

    const cdkDrag = inject(CdkDrag, { self: true });
    const hostElement: HTMLElement = inject(ElementRef).nativeElement;
    
    afterNextRender(() => {
      // 4. 要嘛自生是 dragHanle,要嘛子层有 [cdkDragHandle] element
      //    提醒:不支持动态 [cdkDragHandle] element 哦,我们只在初始化时拿一次而已。
      const dragHandle = hostElement.querySelector<HTMLElement>('[cdkDragHandle]') ?? hostElement;

      // 5. 监听 keydown
      dragHandle.addEventListener('keydown', e => {
        if (this.disabled()) return; // 6 disabled 就不能 keyboard 操作
        if (!e.altKey) return; // 7. 一定要按住 Alt 键
        if(!['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowLeft'].includes(e.key)) return; // 8. 一定是上下左右键
        if (e.target !== e.currentTarget) return; // 9. 冒泡就 skip 

        // 10. 通过 DropList 获取到当前 Drag 的 index,因为换位置需要之前之后的位置 index
        const previousIndex = cdkDropList.getSortedItems().indexOf(cdkDrag); 

        // 11. 按下或按右就表示移动到下一个,所以 index +1, 反过来就是 -1
        let currentIndex = previousIndex + (['ArrowDown', 'ArrowRight'].includes(e.key) ? 1 : -1);

        // 12. 模拟触发 cdkDropListDropped 事件
        //     完整的 CdkDragDrop 需要很多资料,但这里我们只提供了 previousIndex, currentIndex 而已,
        //     针对这个场景已经够 moveItemInArray 使用了。
        cdkDropList.dropped.emit({ previousIndex, currentIndex } as CdkDragDrop<unknown>);

        // 13. -1 移动后会失焦,我不清楚是不是 bug,但懒惰去找原因了,这里手动聚焦 focus 回去就好呗。
        window.requestAnimationFrame(() => (e.target as HTMLElement).focus());
      });
    });
  }
}

效果

 

Drag and drop between multiple drop list

上面例子是一个 drop list,多个 item 在里面换位置。

这里在一个进阶版本,多个 drop list,item 在不同 drop list 间换位置。

组件

export class TestMyDragComponent {
  readonly todoTasks = signal([
    'Fix header styling',
    'Update dependencies',
    'Review pull request',
    'Add error handling',
    'Deploy new build',
  ]);

  readonly doneTasks = signal([
    'Debug login issue',
    'Optimize loading speed',
    'Write unit tests',
    'Refactor codebase',
    'Implement dark mode',
  ]);
}

Template

<div class="task-group">
  <div class="task-list" cdkDropList>
    @for (task of todoTasks(); track task) {
      <div class="task" cdkDrag>{{ task }}</div>
    }
  </div>
  
  <div class="task-list" cdkDropList>
    @for (doneTask of doneTasks(); track doneTask) {
      <div class="task" cdkDrag>{{ doneTask }}</div>
    }
  </div>
</div>

Styles

.task-group {
  display: flex;
  gap: 64px;

  .task-list {
    border: 1px solid black;
    padding: 16px;
    width: 256px;
    display: flex;
    flex-direction: column;
    gap: 16px;

    .cdk-drag-placeholder {
      opacity: 0;
    }

    &.cdk-drop-list-dragging .task:not(.cdk-drag-placeholder) {
      transition: transform 0.25s;
    }
  }
}

.task {
  padding: 16px;
  border: 1px solid black;
  background-color: pink;

  &[cdkDrag] {
    cursor: grab;
  }

  &.cdk-drag-preview.cdk-drag-animating {
    transition: transform 0.25s;
  }
}
View Code

效果

目前 item 只能在自己的 drop list 里换位置,无法跨到其它 drop list,这是因为它们还不认识彼此。

这就好像 drag 必须要认识 drop list 以后,它们才可以携手完成 drag and drop 换位,同样的,不同 drop list 也需要互相认识才能携手完成跨 drop list 的 item 换位。

有两个做法可以让它们互相认识:

  1. CdkDropListGroup 指令

    它的作用是把其下所有 drop list connect 起来,让它们互相认识。

  2. @Input cdkDropListConnectedTo

    你中有我,我中有你。

不管哪一种方式都好,关键就是让它们链接上就可以了。

效果

注:在同一个 drop list 里换位置,它只是改变 placeholder 的 translate 而已,而换 drop list 则是 remove and append placeholder to target drop list。

Handle drop event

虽然 dragging 效果是正确的了,但是 drop 的时候还得更新 view model 丫。

我们在 Template 动点手脚

<div class="task-group" cdkDropListGroup>
  <!-- @Input cdkDropListData 可以存入任何类型的资料,这里我们把对应的 Signal 传进去 -->
  <!-- @Output cdkDropListDropped 统一用 drop 方法来处理  -->
  <div class="task-list" cdkDropList [cdkDropListData]="todoTasks" (cdkDropListDropped)="drop($event)">
    @for (task of todoTasks(); track task) {
      <div class="task" cdkDrag>{{ task }}</div>
    }
  </div>
  <!-- @Input cdkDropListData 可以存入任何类型的资料,这里我们把对应的 Signal 传进去 -->
  <!-- @Output cdkDropListDropped 统一用 drop 方法来处理  -->
  <div class="task-list" cdkDropList [cdkDropListData]="doneTasks" (cdkDropListDropped)="drop($event)">
    @for (doneTask of doneTasks(); track doneTask) {
      <div class="task" cdkDrag>{{ doneTask }}</div>
    }
  </div>
</div>

首先,所有的 drop list 统一使用同一个 drop 方法来处理。

接着我们需要给不同的 drop list 一个识别,这样 drop 方法才能依据不同情况做处理。

@Input cdkDropListData 可以传入任何资料,我们利用它传入那个 drop list 的 view model (tasks Signal),这样不同的 drop list 就有不同的 task signal,这就可以识别了。

然后是 drop 方法

drop(event: CdkDragDrop<WritableSignal<string[]>>) {
  // 这里 container 指的是 drop list
  // 一个 item 从 drop list A 移动到 drop list B
  // 那 previous container 指的就是 drop list A
  // current container 指的就是 drop list B
  // 这里我们把 prev 和 curr 的 container data (也就是 tasks Signal) 拿出来。
  const prevTasks = event.previousContainer.data;
  const currTask = event.container.data;

  if (event.previousContainer === event.container) {
    // 如果 prev 和 curr 相同,代表 item 是在同一个 drop list 里移位
    // 那我们就用 moveItemInArray 函数处理就可以了
    const clonedTasks = [...currTask()];
    moveItemInArray(clonedTasks, event.previousIndex, event.currentIndex);
    currTask.set(clonedTasks);
  } else {
    // 如果 prev 和 curr 不同,代表 item 已经移动到另一个 drop list 了
    // 此时,两个 drop list 都需要 update
    // 一个 remove, 一个 add
    const clonedPrevTasks = [...prevTasks()];
    const clonedCurrTasks = [...currTask()];
    // transferArrayItem 函数是 CDK Drag and Drop built-in 的函数,专门用来处理跨 drop list 的换位
    transferArrayItem(clonedPrevTasks, clonedCurrTasks, event.previousIndex, event.currentIndex);
    prevTasks.set(clonedPrevTasks);
    currTask.set(clonedCurrTasks);
  }
}

注释已经解释清楚了,简单说:

  1. 比对 prev 和 curr container 看是 single drop list 换位,还是跨 drop list 换位

  2. single drop list 就用回 moveItemInArray 函数

  3. 跨 drop list 就用 transferArrayItem 函数

最终效果

 

Drag and drop with scrolling

CDK Drag and Drop 支持 scrolling。

效果

dragging 的时候可以使用 mouse wheel 移位,或者当 drag item 靠近两端口时,CDK Drag and Drop 会修改 scrollTop 移位。

注:CDK Drag and Drop 是依靠 ScrollDispatcher 来监听 scroll 事件的,所以我们要记得在 overflow element 上添加 CdkScrollable 指令 哦。

 

Mixed orientation drag and drop

Angular 在 v18.1 推出了 Mixed orientation drag and drop,以前只支持 1 direction,要嘛 vertical,要嘛 horizontal,不可以 mixed,但现在可以了。

组件

export class TestMixedOrientationComponent {
  readonly tasks = signal([
    'Fix header styling',
    'Update dependencies',
    'Review pull request',
    'Add error handling',
    'Deploy new build',
    'Debug login issue',
    'Optimize loading speed',
    'Write unit tests',
    'Refactor codebase',
    'Implement dark mode',
  ]);

  drop(event: CdkDragDrop<unknown>) {
    const clonedTasks = [...this.tasks()];
    moveItemInArray(clonedTasks, event.previousIndex, event.currentIndex);
    this.tasks.set(clonedTasks);
  }
}
View Code

Template

<div class="task-list" cdkDropList (cdkDropListDropped)="drop($event)">
  @for (task of tasks(); track task) {
    <div class="task" cdkDrag>{{ task }}</div>
  }
</div>
View Code

Styles

.task-list {
  border: 1px solid black;
  padding: 16px;
  width: 512px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;

  .cdk-drag-placeholder {
    opacity: 0;
  }

  &.cdk-drop-list-dragging .task:not(.cdk-drag-placeholder) {
    transition: transform 0.25s;
  }
}

.task {
  padding: 16px;
  border: 1px solid black;
  background-color: pink;

  &[cdkDrag] {
    cursor: grab;
  }

  &.cdk-drag-preview.cdk-drag-animating {
    transition: transform 0.25s;
  }
}
View Code

代码和上面的例子都差不多,只是改成了 grid layout。

效果

可以看到,dragging 时,整个 layout 都乱套了。

原因很简单,CDK Drag and Drop 只会把它当成 one direction (默认是 vertical) 来处理,在换位时只会设置 translateX,不会设置 translateY。

所以,它的位置是绝对不可能正确的。

我们可以透过设置 CdkDropList @Input orientation 让它支持 mixed orientation。

<div class="task-list" cdkDropList (cdkDropListDropped)="drop($event)" cdkDropListOrientation="mixed">

它支持 'vertical' (默认),'horizontal' 还有 'mixed'。

效果

注:'vertical' 和 'horizontal' 是透过 set translate 来实现移位的,移动时可以带有 animation。然而 'mixed' 却不是,mixed 是透过 remove and re-insert element 来实现移位的,所以它在移位时无法配上 animation (这显然是 Angular Material 团队不给力😒)。

v18 旧版本 (暂留)

说明:本篇撰写于 Angular Material v18,当时还不支持 mixed orientation drag and drop,以下是当时写的相关内容,暂时保留做纪念,以后会删除。

drag and drop 换位置只支持一个方向,要嘛 vertical (我们上面的例子都是 vertical) 要嘛 horizontal (可以透过 @Input cdkDropListOrientation 做配置),不可以像 grid layout 那样 2 directions。

这是一个常年霸榜的 feature request

很遗憾,Angular Material 团队到今天都没有意愿要解决。

我们来看具体例子。

组件

export class TestMixedOrientationComponent {
  readonly tasks = signal([
    'Fix header styling',
    'Update dependencies',
    'Review pull request',
    'Add error handling',
    'Deploy new build',
    'Debug login issue',
    'Optimize loading speed',
    'Write unit tests',
    'Refactor codebase',
    'Implement dark mode',
  ]);

  drop(event: CdkDragDrop<unknown>) {
    const clonedTasks = [...this.tasks()];
    moveItemInArray(clonedTasks, event.previousIndex, event.currentIndex);
    this.tasks.set(clonedTasks);
  }
}
View Code

Template

<div class="task-list" cdkDropList (cdkDropListDropped)="drop($event)">
  @for (task of tasks(); track task) {
    <div class="task" cdkDrag>{{ task }}</div>
  }
</div>
View Code

Styles

.task-list {
  border: 1px solid black;
  padding: 16px;
  width: 512px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;

  .cdk-drag-placeholder {
    opacity: 0;
  }

  &.cdk-drop-list-dragging .task:not(.cdk-drag-placeholder) {
    transition: transform 0.25s;
  }
}

.task {
  padding: 16px;
  border: 1px solid black;
  background-color: pink;

  &[cdkDrag] {
    cursor: grab;
  }

  &.cdk-drag-preview.cdk-drag-animating {
    transition: transform 0.25s;
  }
}
View Code

代码和上面的例子都差不多,只是改成了 grid layout。

效果

可以看到,dragging 时,整个 layout 都乱套了。

原因很简单,CDK Drag and Drop 只会把它当成 one direction (默认是 vertical) 来处理,在换位时只会设置 translateX,不会设置 translateY。

所以,它的位置是绝对不可能正确的。

Workaround

社区提供了一个临时解决方案,这里我们分析一下它的原理。

首先,它把所有的 item 都 wrap 上了一层 drop list。

所以在 drag and drop 时,它并不是在一个 drop list 里面换位置,而是跨 drop list 换位置。

那依照这个路线走的话,当 item A 被 drag 到 item B 时,item A 会跑进去 item B 的 drop list,

此时 drop list A 应该变成空的,drop list B 里有 item A 和 B。

但是结果却不是这样,因为它在 drop list enter 时动了手脚。

我们分两段来看,第一段的任务是让 drop list A 和 drop list B 换位。(提醒:此时 drop list A 内是空的,drop list B 里有 item A 和 B)

这个换位是纯粹的 DOM manipulation,和 CDK Drag and Drop 没有任何关联。

第二段的任务是把 item A 放回 drop list A,这是透过 CDK Drag and Drop 私有功能 (DropListRef.enter) 完成的。

整个过程大概是这样

非常巧妙的思路👍

 

逛一逛源码

我们稍微逛一逛它的结构就好了,毕竟它都不完整嘛。

万物的开始是 CdkDrag 指令,它的源码在 drag.ts

在 constructor 阶段,inject DragDrop Service 然后调用 createDrag 方法创建一个 DragRef 实例。

DragDrop Root Level Provider 长这样,源码在 drag-drop.ts

没什么特别的,只是简单的 new DragRef 或 new DropListRef 而已。

DragRef 的源码在 drag-ref.ts

withRootElement 方法

这里监听了 mouse down。所谓的 dragging 第一个动作就是 mouse down,然后是 mouse move,最后是 mouse up。

_pointerDown 方法

_initializeDragSequence 方法

我们看重点就好,里面监听了 mouse move 和 mouse up 事件。

DragDropRegistry 是一个 Root Level Provider,源码在 drag-drop-registry.ts

pointerMove 和 pointerUp 是 RxJS Subject,而它的 source (事件源) 来自 document mouse move 和 mouse up 事件。

回到 DragRef._pointerMove 方法

假如这个 drag item 没有在任何 drop list 里面,那只需要 set drag item transform translate 就可以了。

_applyRootElementTransform 方法

好,上面这个就是最简单的 free dragging 例子,从 mouse down > move > set translate,up 的部分我们就不看了。

接着看看,drag and drop 的例子。

前半段都一模一样,一直到 move 的时候它们的处理方式就不同了。

回到 DragRef._pointerMove 方法

_startDragSequence 方法

这里主要是创建了 placeholder 和 preview element,还有 append 它们到相应的位置。

_createPlaceholderElement 函数长这样

如果我们没有传入指定的 CdkDragPlaceholder 指令作为 ng-template,那这里默认会 clone from drag item。

回到 DragRef._pointerMove 方法

_updateActiveDropContainer 方法

接着

这个 _dropContainer 的类型是 DropListRef,上面有提到过。

CdkDropList 指令在 constructor 阶段会透过 DragDrop Service 创建出 DropListRef,源码在 drop-list.ts

CdkDrag 指令在 constructor 阶段会 inject CdkDropList 作为 _dropContainer,这样它们就串联起来了。

我们继续看 DropListRef._sortItem 方法,源码在 drop-list-ref.ts

里面的关键是调用了 _sortStrategy 的 sort 方法。

_sortStrategy 是一个抽象类

它的具体实现是 SingleAxisSortStrategy 和 MixedSortStrategy,顾名思义,一个是 for vertical / horizontal,另一个是 for mixed orientation。

我们先看看 SingleAxisSortStrategy.sort 方法

总之就是一堆 formula 计算之后给每个 item set translate 换位。

MixedSortStrategy.sort 方法

总之就是一堆 formula 计算之后做 remove and re-insert element 来移位。

好,源码就逛到这里,以上就是本篇教程涉及到的相关源码。

 

总结

虽然 CDK Drap and Drop 非常简陋,但依然可以在真实项目中排上用场,比如

个人意见,目前最好不要太重用或依赖它,拿它来应付 1 direction 1 drop list 的场景就好,太复杂怕会掉坑🙂。

另外,本篇没有介绍完所有 CDK Drag and Drop 的功能,建议读者也去看一下官方的文档。

 

 

目录

上一篇 Angular Material 18+ 高级教程 – Material Tooltip

下一篇 Angular Material 18+ 高级教程 – Material Form Field

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻

 

posted @ 2024-06-06 20:09  兴杰  阅读(467)  评论(2编辑  收藏  举报