3

Using the example from CDK drag and drop, I want to add a preview of the dragged element with left and top positions without the transform style.

HTML

<div class="example-boundary">
  <div class="example-box" cdkDragBoundary=".example-boundary" cdkDrag>
    I can only be dragged within the dotted container
  </div>
</div>

<button> Preview the dragged element </buttona>

TypeScript

import {Component} from '@angular/core';
import {CdkDrag} from '@angular/cdk/drag-drop';

/**
 * @title Drag&Drop boundary
 */
@Component({
  selector: 'cdk-drag-drop-boundary-example',
  templateUrl: 'cdk-drag-drop-boundary-example.html',
  styleUrls: ['cdk-drag-drop-boundary-example.css'],
  standalone: true,
  imports: [CdkDrag],
})
export class CdkDragDropBoundaryExample {}

Current state

When you drag the element you have this div in DOM

<div _ngcontent-ng-c2320506461="" class="example-boundary">
  <div _ngcontent-ng-c2320506461="" cdkdragboundary=".example-boundary" cdkdrag="" class="cdk-drag example-box" style="transform: translate3d(202px, -2px, 0px);">
I can only be dragged within the dotted container
 </div>
</div>

Expected result

When you drag the element and click the preview button it should open the preview element which looks like this.

<div class="example-boundary">
  <div class="example-box" style="left: 96.13%; top: 9.92%; display: block;">
   Now I can't be dragged, sorry
  </div>
</div>

Meaning the transform style should be replaced with left and top positions.

Mayank Kumar Chaudhari
  • 16,027
  • 10
  • 55
  • 122
The Dead Man
  • 6,258
  • 28
  • 111
  • 193
  • which version of Angular are you using? – luiscla27 Jul 12 '23 at 23:05
  • 5
    The only one flooding the question with unrelated comments is you, @Ali. – Shadow The GPT Wizard Jul 19 '23 at 09:16
  • 3
    Looks like OP here forgot about the bounty, it will be auto awarded in 10 hours. – Shadow The GPT Wizard Jul 19 '23 at 11:12
  • The bounty (500 reputation points worth) attracted a [ChatGPT](https://meta.stackoverflow.com/questions/421831/temporary-policy-chatgpt-is-banned) plagiariser. – Peter Mortensen Jul 19 '23 at 17:03
  • Re "` – Peter Mortensen Jul 19 '23 at 17:08
  • I just inspected OP questions and I was surprised to see that I remembered some questions... that's because they were high bounty questions which I gave a thought to answer. Just like this question, he often doesn't award the bounty. SO should have another kind award for that behavior. – luiscla27 Jul 20 '23 at 21:10

2 Answers2

5

@angular/cdk/drag-drop internally uses transform property for placing the box. Whether top, left and position properties are used or transform is used in internal detail of the package and should be understood to be encapsulated. You can achieve the same results using both ways. If there is anything specific you want to do with top and left properties, you can either compute them from transform and elements original position or code drag function in pure JS.

Following is a pure JS version of what you want to do.

const box = document.querySelector('.example-box');
const boundary = document.querySelector(box.getAttribute('cdkDragBoundary') || '');
const posDisplay = document.querySelector('#pos');

const offset = {
  x: 0,
  y: 0
};

const onMouseMove = (e) => {
  e.preventDefault();
  const [cx, cy] = [e.clientX, e.clientY];
  const {
    width,
    height
  } = box.getBoundingClientRect();
  let top = cy - offset.y;
  let left = cx - offset.x;

  const {
    width: bw,
    height: bh
  } = boundary?.getBoundingClientRect();
  top = Math.min(top, (bh || innerHeight) - height);
  top = Math.max(top, 0);
  left = Math.min(left, (bw || innerWidth) - width);
  left = Math.max(left, 0);
  box.style.top = top + 'px';
  box.style.left = left + 'px';
  posDisplay.innerText = `left: ${left}px, top: ${top}px`;
};

box.onmousedown = e => {
  e.preventDefault();
  offset.x = e.clientX - box.offsetLeft;
  offset.y = e.clientY - box.offsetTop;
  window.onmousemove = onMouseMove;
  window.onmouseup = () => {
    window.onmousemove = null;
    window.onmouseup = null;
  };
}
.example-boundary {
  position: relative;
  border: 1px dotted gray;
  width: 80vw;
  height: 80vh;
  margin: 0 10vmin;
}

.example-box {
  position: absolute;
  width: 200px;
  padding: 10px;
  border-radius: 10px;
  border: 1px solid green;
  cursor: grab;
}

#pos {
  height: 50px;
  padding: 0 10px;
}
<p id="pos">left: 0, top: 0</p>
<div class="example-boundary">
  <div class="example-box" cdkDragBoundary=".example-boundary">
    I can only be dragged within the dotted container
  </div>
</div>
Mayank Kumar Chaudhari
  • 16,027
  • 10
  • 55
  • 122
1

TLDR;

I've made the two following working examples which does what you ask. In both cases, the top and left positions are computed using the following regular expression by capturing groups 1 and 2 transform:\s*translate3d\((.+?),(.+?),.+?\).

  1. Cloned DOM, this is my recommended answer, it clones the Angular generated HTML and removes unwanted attributes manually.

  2. Stored data, as you requested, it stores DOM position, width, height, etc. in a variable and then renders it.


Approach #1 (Cloned DOM)

The following code clones the Angular generated HTML and removes unwanted attributes manually. There's more details in the comments in code.

In case that you're simply going to re-render what was dragged somewhere else, this approach would be my personal recommendation as it removes the complexity layer of managing attributes.

TS:

/**
 * 1. Added a flag called `isPreviewShown` which toggles which HTML is visible, either the preview or the original
 * 2. Added a method called `showPreview` which clones the draggable contents and then manually removes unwanted attributes (you'll have to maintain this logic, so you may remove other attributes you don't need)
 * 3. Replaced `cssText` style attributes from `translate3d` to `top/left` values. I used the following [regular expression][2] for it `transform:\s*translate3d\((.+?),(.+?),.+?\)` and the replaced it to `left:$1; top:$2`
 **/
isPreviewShown = false;

showPreview() {
    // clone the contents
    this.previewContainer.nativeElement.innerHTML = this.exampleBoundary.nativeElement.innerHTML;
    // clear unwanted attributes
    document.querySelectorAll('.preview-container *').forEach((content: HTMLElement) => {
      const attrs = content.getAttributeNames();
      for (var attr in attrs) {
        if (
          attrs[attr].indexOf('cdk') !== -1   // <-- remove cdk related attributes
          || attrs[attr].indexOf('_ng') === 0 // <-- remove angular relates attributes
          || attrs[attr].indexOf('ng-') === 0 // <-- remove more angular relates attributes
        ) {
          console.log('->> removed: ', attrs[attr])
          content.removeAttribute(attrs[attr]);
        }
      }
      // transform/translate3d --> top/left
      content.style.cssText = content.style.cssText.replace(/transform:\s*translate3d\((.+?),(.+?),.+?\)/i, 'left:$1; top:$2') 
      // remove cdk-drag class
      content.classList.remove('cdk-drag')
    });
    console.log('>> Result: ', this.previewContainer.nativeElement.innerHTML)
    // show the preview
    this.isPreviewShown = true;
    this.cdr.detectChanges();
}

HTML:

<!--
1. Added a `preview-container` which is where the preview is going to be shown
2. Added a `main-container` which is meant to make the `preview` to overlap the original content
3. Added an extra button called "Hide preview element" which hides the preview so you can drag again
-->
<div class="main-container">
  <div class="example-boundary" #exampleBoundary>
    <div class="example-box" cdkDragBoundary=".example-boundary" cdkDrag>
      I can only be dragged within the dotted container
    </div>
  </div>
  <div class="preview-container example-box" #previewContainer [hidden]="!isPreviewShown">
  </div>
</div>
<br>
<button (click)="hidePreview()" [hidden]="!isPreviewShown"> Hide preview element </button>
<button (click)="showPreview()" [hidden]="isPreviewShown"> Preview the dragged element </button>

CSS:

/**
 * 1. Add `position: relative` to the `main-container` so the `preview-container` size and position are shown relative to the main container. 
 * 2. Had to add `:host ::ng-deep` to class `.example-box` so the CSS can be shared by both preview and original content (an alternative to this, is to set all style attributes inline by using [`ngStyle`][4] so you'll don't need a css class)
 */
:host ::ng-deep .example-box {
   /* no changes here */
}
.main-container {
  position: relative;
  width: 400px;
  height: 400px;
}
.preview-container {
  position: absolute;
  background: white;
  width: 100%;
  height: 100%;
  top: 0px;
  left: 0px;
  z-index: 1;
}

Approach #2 (Stored Data)

This approach does exactly as you requested in comments, it stores DOM position, width, height, etc. in a variable and then renders it by iterating a variable.

In case that you're going to manipulate individual items later (like adding configurable stuff that is not present while dragging) then this approach would be best than #1.

TS:

/**
 * 1. Added an interface called `IDragAndDropItem` with all the attributes that are going to be stored.
 * 2. Added a flag called `isPreviewShown` which toggles which HTML is visible, either the preview or the original
 * 3. Added a method called `showPreview` which iterates the first DOM level and stores the desired attributes in the array `previewItems`
 * 4. Computed the `top` and `left` values from the `cssText` style attribute, matched the following [regular expression][2] captured group 1 and 2: `transform:\s*translate3d\((.+?),(.+?),.+?\)`
 */
export interface IDragAndDropItem {
  width: string;
  height: string;
  left: string | null;
  top: string | null;
  text: string;
}

isPreviewShown = false;
previewItems: Array<IDragAndDropItem> = [];

showPreview() {
    this.previewItems = [];
    // save the contents
    document
      .querySelectorAll('.example-boundary > *') // <-- purposely doesn't support nested DOM
      .forEach((content: HTMLElement) => {
        const position = content.style.cssText.match(
          /transform:\s*translate3d\((.+?),(.+?),.+?\)/i
        );
        this.previewItems.push({
          width: content.offsetWidth + 'px',
          height: content.offsetHeight + 'px',
          left: position ? position[1] || null : null,
          top: position ? position[2] || null : null,
          text: content.innerText
        })
      });
    // show the preview
    this.isPreviewShown = true;
    this.cdr.detectChanges();
    // show html
    this.resultHtml.nativeElement.innerText =
      this.previewContainer.nativeElement.innerHTML;
}

HTML:

<!--
1. Added a `preview-container` which is where the `previewItems` is iterated and rendered.
2. Added a `main-container` which is meant to make the `preview` to overlap the original content
3. Added an extra button called "Hide preview element" which hides the preview so you can drag again
-->
<div class="main-container">
  <div class="example-boundary" #exampleBoundary>
    <div class="example-box" cdkDragBoundary=".example-boundary" cdkDrag>
      I can only be dragged within the dotted container
    </div>
  </div>
  <div class="preview-container example-boundary" #previewContainer [hidden]="!isPreviewShown">
    <div class="preview-box" *ngFor="let item of previewItems" [ngStyle]="{left: item.left, top: item.top, width: item.width, height: item.height}">
      {{ item.text }}
    </div>
  </div>
</div>
<br>
<button (click)="hidePreview()" [hidden]="!isPreviewShown"> Hide preview element </button>
<button (click)="showPreview()" [hidden]="isPreviewShown"> Preview the dragged element </button>

CSS:

/**
 * 1. Add `position: relative` to the `main-container` so the `preview-container` size and position are shown relative to the main container. 
 * 2. Had to share styles from `example-box` with `preview-box`, otherwise every `style` attribute should be also stored.
 */
.example-box, .preview-box {
   /* no changes here */
}
.main-container {
  position: relative;
  width: 400px;
  height: 400px;
}
.preview-container {
  position: absolute;
  background: white;
  width: 100%;
  height: 100%;
  top: 0px;
  left: 0px;
  z-index: 1;
}
luiscla27
  • 4,956
  • 37
  • 49
  • Hej thanks for your effort, but this does not work as expected and the solution is not good by the way, I think the better way is to save all data in an array eg positions, width, height, etc and then on the preview component just loop through the array and display the dragged element – The Dead Man Jul 13 '23 at 10:14
  • The approach you mention might end up a bit *too complicated* when adding more draggable elements, or a lot more complicated if you add nested ones... I prefer the approach I posted because the rendering part is done already by Angular, and not by *my self* iterating data. However, I suspect that your question might came short on what you need... It might be a good idea to ask a new one but including your whole requirement, that would be if you *do need* to store individual elements for a reason. If you don't, then you should consider to give a chance to this answer. Good luck! – luiscla27 Jul 13 '23 at 16:47
  • I think its the easiest way though, just give an id of each draggable element and save its configurations meaning positions, width , height , text etc and then show them on preview – The Dead Man Jul 13 '23 at 21:17
  • I might give it a try to the approach you describe tomorrow night. However, I disagree about *that way* being the best choice. It might be the easiest approach for the example template... but it can easily get nasty with more elaborated drag & drop scenarios. – luiscla27 Jul 13 '23 at 23:18