I am building a drag and drop feature in my application where the drop area looks like this:
<template>
<div class="layers-panel" #dragover="onDragOver" #drop="onDragDrop">
<layer
v-for="(layer, index) in layers"
:key="layer.name"
:info="layer"
:offset="index"
ref="layer"
></layer>
</div>
</template>
Then the draggable items look like this:
<template>
<div class="layer" draggable="true" #dragstart="onDragStart">
<!-- Content of draggable element -->
</div>
</template>
In my drag over event, I want to get the current component that the element relates to, so when I drag an item over the <div class="layer"...> element I would like to get that component. I am not sure how to get access to this since an event is passed to the #dragover="onDragOver" event which only gives me access to html elements and not Vue components.
This is what my current dragover event looks like:
public onDragOver(evt: DragEvent) {
evt.preventDefault()
if (!evt.target) return
let layer = (evt.target as HTMLElement).closest('.layer') as HTMLElement
}
How can I get the component that that element belongs to?
Vue certainly allows you to access a component instance however I don't think it's necessary to reaching your actual goal: reordering of components via drag-and-drop.
Drag-and-drop means we're limited by what's afforded by DragEvent:
We can access the target element e.g. the element being dragged or dropped on
We can set arbitrary data via DataTransfer
You can't determine where the element is dropped because your drop element contains all the layers. Instead if each layer is a drop element, then you can determine which one it is via dragover via event.target.
We also need to identify which element corresponding to the layers array we're dragging/dropping on. This can be achieved by binding the index of each layer as an attribute so we can access it via event.target.
To keep it simple, I'll use the id attribute so I can just access it later via event.target.id:
<div
v-for="(layer, idx) in layers"
:key="idx"
:id="idx"
draggable="true"
#dragstart="dragstart"
>
<div
:id="idx"
class="draggable"
#dragover="dragover"
#drop="drop"
>
{{ layer }}
</div>
</div>
Note I bind the same index to id so I can correctly identify the target layer.
We'll also need to ensure we can identify the element that was dragged since on drop we'll only have the drop element via event.target. We can do this by setting data in DataTransfer when we first start the drag:
dragstart(event) {
event.dataTransfer.setData("selectedIdx", event.target.id);
},
Now on drop we have access to both and can manipulate layers as we desire:
drop(event) {
var selectedIdx = event.dataTransfer.getData("selectedIdx");
var targetIdx = event.target.id;
var selected = this.layers.splice(selectedIdx, 1)[0];
this.layers.splice(targetIdx, 0, selected);
},
Example codepen
Related
I'm setting up an Ionic (4) app, where you can drag and drop elements from a toolbar into a window. I want to change the dropped elements depending on their types. I'm using ng2-dragula.
For exmaple I want to drop an element <ion-chip></ion-chip> and when it's dropped it should be something like <ion-card dragula="DRAGGABLE"></ion-card>.
-I tried changing the DOM outerHTML during an event ( https://github.com/valor-software/ng2-dragula/blob/master/modules/demo/src/app/examples/02-events.component.ts ), but then the dragula inside the new created element is not active.
-I tried *ngIf but this also seems not to load dynamically.
What other possibilities do I have?
<div dragula="DRAGULA_EVENTS">
<div>content</div>
</div>
BAG = "DRAGULA_EVENTS";
subs = new Subscription();
export class EventsComponent{
public constructor(private dragulaService: DragulaService){
this.subs.add(this.dragulaService.drop(this.BAG)
.subscribe(({el,source,target})=>{
el.outerHTML = "<ion-card dragula='DRAGULA_EVENTS'>content</ion-card>"
}
}
})
}
I expect that the new DOM element has the property dragula like i set it on the outerHTML-tag.
I figured it out (works during dragging or dropping):
Nest the dragula element in a new
Create inside page.component.ts :
this.subs.add(this.dragulaService.cloned(this.BAG).subscribe(({clone})=>{
Inside of this, with DOM methods, creating a new Element var new_El = document.createElement('ion-card')
Remove all Childs of the clone 'clone.removeChild(..)`
Bind the new Element inside the clone Element clone.appendChild(new_El)
I'm trying to make certain "cards" that display next to each other, and when the card is clicked, I want to get the id of the card. However, my function gets the id of the child elements when I click it, which is undefined, rather than of the parent card, unless I click on the border of the card div. How do I make sure it gets the id of the card specifically?
Here is the HTML:
<div id="parentCard" onclick="getID(event)">
<div id="child1">I'm a child</div>
<div id="text">
<p>Text 1</p>
<p>Text 2</p>
<p>Text 3</p>
</div>
</div>
Javascript:
function getID(event){
console.log(event.target.id);
}
Grab the target element of the event (the element that was clicked).
Here I've used destructuring to get the id, tagName, and parentNode properties from the target.
If the clicked element is a card div element log the id, otherwise log the id of the parent element.
Note: this assumes that the cards won't have nested HTML any deeper than they are in your example or parentNode won't apply to the right parent element.
const parent = document.getElementById('parentCard');
parent.addEventListener('click', handleClick, false);
function handleClick(e) {
const { id, tagName, parentNode } = e.target;
console.log(tagName === 'DIV' ? id : parentNode.id);
}
<div id="parentCard">
<div id="child1">I'm a child</div>
<div id="text">
<p>Text 1</p>
<p>Text 2</p>
<p>Text 3</p>
</div>
</div>
Try with Event.currentTarget which
Identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to event.target which identifies the element on which the event occurred.
function getID(event){
console.log(event.currentTarget.id);
}
<div id="parentCard" onclick="getID(event)">
<div id="child1">I'm a child</div>
<div id="text">
<p>Text 1</p>
<p>Text 2</p>
<p>Text 3</p>
</div>
</div>
Every time the user clicks somewhere on a page, the user agent will determine which "non-compound" element was clicked on and fire an event of type "click" event with that element as event target.
A "non-compound" element here means one that does not have descendant elements (it may have descendant nodes, like text, for instance).
Since your cards will typically be compound elements -- containing other elements (which may contain other elements in turn) -- there is no guarantees about which of these elements will be the click event target.
The user agent is a machine and cannot think for you and know that that the user "clicks on a card". If the card contains a button or a link or an image, all the user agent will do is tell you which of these the user has clicked on, through the means of the target property on the event -- you need to determine the fact that from your perspective, a card was clicked on.
There are at least two ways to determine said fact:
Find out which card element is or contains the clicked element:
function get_card(element) {
for(; element; element = element.parentElement) {
if(is_card(element)) return element;
}
}
How do you know, traversing ancestor list, that an element is a card? As far as the document object model is concerned, it's just a div element, with an onclick attribute and an ID. You may have div elements with defined id and onclick attributes in your document that are not cards.
Adding a class to an element is a natural way to, well, classify it:
<div class="card">
<!-- descendant elements -->
</div>
With the above we decide and now assume that every card element will include "card" in the list of its class names. Now you can write your is_card function (used in get_card), as follows:
function is_card(element) {
return element.classList.contains("card");
}
With a working get_card(element) function in your program you can now reliably answer the question "which card, if any, contains element?". In combination with a single event listener on, say, the window object you can now know which card the user clicks on:
addEventListener("click", function(ev) {
var card = get_card(ev.target);
if(card) {
/// A card was clicked, do something about it.
}
}
As you can see, you don't even need to add listeners to every card, adding one to one container element of your choice (or window as is the case above) is all you need with this solution.
Now, this is a case of a so-called linear-time algorithm -- the "deeper" your cards (descendant elements containing other descendant elements in turn), the longer average time it will take to locate the card for a descendant element. But it's nothing to worry about with "click" handling and performance of modern JavaScript runtimes, if I am allowed to be general here.
Use the [so-far experimental] CSS4 and the pointer-events: none rule for card's children. Assuming every card includes the class "card", you can specify that no descendant element will have pointer events enabled -- these will never fire "click" events, the card element will instead:
.card > * {
pointer-events: none;
}
The support for pointer-events is still experimental but I know that recent Google Chrome and Firefox do support it. You have to be careful here though -- like, are you prepared to disable interactivity for some of card's elements? What if you have form elements there, which are typically interactive? You need to assess these kind of requirements yourself.
There is a third solution, but I consider it inferior to at least the first solution. For the sake of completeness, assigning an event listener on each card may be wasteful, especially if there are many cards on each page. However, in any case, if you attach a "click" listener to a card like you yourself did, the element the listener for the event type "click" was added to, is available with the ev.currentTarget property! Meaning that no matter which descendant element fires the "click" event, the event will reach the card element, through its capture and bubbling phases, and the currentTarget property value will always refer to the element the listener is added to -- in this case the card element. Of course, this solution implies that the listener is attached to every card.
I need to get the parent element which has got the draggable attribute assigned to it, as this also has some other data attributes I need.
However if I click and drag on a child element of course this is the target.
Is there a way to do this without a horrid findAncestor loop?
<div class="panel" draggable="true" data-move-id="1">
<div data-move-handle class="panel-heading"></div>
<div class="panel-block"></div>
<div class="panel-block"></div>
<div class="panel-block"></div>
<div class="panel-block"></div>
</div>
Or, better yet, is there a way I can limit the draggable handle to the div with the data-move-handle attribute, however I still want the entire div to move. Then I can move the data-move-id to that div.
To have data available when you drop, from your 'dragged' element, you have to include it in your event. This can get tricky with cross browser implementations, especially if you have to support legacy (i.e. IE) browsers.
const startDrag(event) {
// 'this' is the 'draggable' you are attaching this method to,
// so if you need the data attribute just use this.getAttribute()
// get it.
// get the data you need to transfer with your object, and then
event.dataTransfer.setData('text', JSON.stringify(somesetofdata));
}
const drop(event) {
let data = event.dataTransfer.getData('text');
data = data ? JSON.parse(data) : data;
// do something with it
}
That's sorta the gist. Google 'Pure Javascript Drag and Drop' for a lot of example code. Here's a really simple example.
I've been experimenting with Mutation Observers, so far I can apply the relevant functions to react upon adding, removing elements and so on. Now I am wondering, is there a way to target a specific change within a specific style attribute? I know I can observe for attribute changes, but I just don't know how to observe for a specific attribute modification. For example, if the z-index value of #childDiv changes to 5678, modify the style of the parentDiv in order for it to be shown.
<div id="parentDiv" style="display:none;">
<div id="childDiv" style="z-index:1234;">
MyDiv
</div>
</div>
As per the documentation use attributeFilter array and list the HTML attributes, 'style' here:
var observer = new MutationObserver(styleChangedCallback);
observer.observe(document.getElementById('childDiv'), {
attributes: true,
attributeFilter: ['style'],
});
var oldIndex = document.getElementById('childDiv').style.zIndex;
function styleChangedCallback(mutations) {
var newIndex = mutations[0].target.style.zIndex;
if (newIndex !== oldIndex) {
console.log('new:', , 'old:', oldIndex);
}
}
Sorry for offtop.
Conception React is no mutations. If you need to listen some changes of some element (style for example). You can use componentDidUpdate and get element from #refs.parentDiv (set ref before this in render function <div id="parentDiv" ref="parentDiv" style="display:none;">) and after check style and set you z-Index value before new render.
On my webpage I have a DIV of the class "editor" that I copy into a variable.
editorTemplate = $('.editor');
The DIV looks like this (simplified):
<div class="editor">
<div>
Title: <span class="title" id="title"> the title goes here </span><br />
<select class="recording_list" id="recording_list">
<option value="1">Pos 1</option>
<option value="2">Pos 2</option>
...
</select>
</div> <!-- class=editor -->
Later I want to create a series from that div by adding it to the page:
$(editArea).append(editorTemplate);
So far so good.
But I want to change some attributes - like the IDs of the fields, some text and the selected element of the option box - before pasting the editor template onto the page.
I can change the ID of the edit template with
$(myEdit).attr("id", "edit" + nEditors);
But I don't know how to access the INNER elements of the template, e.g. the ID and the text of the "title" field.
After the template is pasted into the page I can say
$('#title').attr("id", "title" + nEditors);
$('#title').html("the new text");
...
Is it possible to make these changes BEFORE I paste the template into the page?
You can make use of the JQuery.children() method.
var editorTemplate = $('.editor');
editorTemplate.children('<selectors to uniquely identify the child object>').<method to update the content accordingly>
So then we could do something like this...
count=1;
editorTemplate.children('span#title').html('<Update HTML here>').attr('id','title_'+count);
UPDATE:
I just noticed that your elements are at multiple levels so using .find() would be ideal as it can traverse down multiple levels to select descendant elements (grandchildren, etc.) as well.
You are not copying the elements into a variable.
editorTemplate = $('.editor');
The above creates a jQuery wrapper with a set of pointers which point to the DOM elements. The wrapper allows you to execute jQuery methods targeting the DOM elements.
If you do editorTemplate.find("#title").attr("id", "newId") on that it changes the id attribute of the element you are currently pointing at in the DOM not a new copy.
When you are planning on doing this later:
$(editArea).append(editorTemplate);
The above will not append a new copy of the DOM elements but instead will be moving the elements you point at through the editorTemplate wrapper from their original location in the DOM to the new location in the DOM editArea is referencing.
If you planning on making duplicates of some elements within editorTemplate to append them later you would use jQuery clone(), similar to this:
// use clone(true, true) to also clone any attached events
var editorTemplate = $('.editor').clone();
// Using .find you can change attribute in your new copy/clone
editorTemplate.find("#title").attr("id", "title" + nEditors).html("the new text");
// append your new copy/clone
$(editArea).append(editorTemplate);
You cam use find method to get access to your elements:
var editorTemplate = $('.editor');
$(editorTemplate).find('#title').attr('id', 'title' + nEditors).html('the new text');
editorTemplate.clone()
.attr({})
.find("select").attr({id:"whatever"}).end()
.find("span").....end()
.appendTo($(editarea))
i hope you get the idea