Kendo UI DragAndDrop TreeView item to a ListView - drag-and-drop

I have a requirement to enable drag and drop from a kendo-ui tree view to a templated list view.
I've tried the following:
1.Enabling dragAndDrop on the treeview and configuring the listview as a kendoDropTarget
2.Disabling dragAndDrop on the treeview and instead configuring that control as kendoDraggable to the listview configured as a kendoDropTarget
<div>
<div id="treeview">
</div></div>
<div id="favorites-window" style="height:185px;width:1170px">
<div class="report-reader" style="height:185px;width:1170px;overflow:auto">
<div id="listView"></div>
</div>
</div>
$("#favorites-window").kendoWindow({
width: "1180",
height: "185",
resizable: false,
draggable: false,
actions: ["Custom"],
title: "Favorites"
});
$("#listView").kendoListView({
selectable: "single",
navigatable: false
}).kendoDropTarget({
drop: function (e) {
console.log(e);
var item = getObjects(nucleusTreeJsonData, 'text', e.draggable.hint.text());
$("#listView").data("kendoListView").dataSource.add(item);
}
});
var inlineDefault = new kendo.data.HierarchicalDataSource({
data: [
{ text: "Furniture", items: [
{ text: "Tables & Chairs" },
{ text: "Sofas" },
{ text: "Occasional Furniture" }
] },
{ text: "Decor", items: [
{ text: "Bed Linen" },
{ text: "Curtains & Blinds" },
{ text: "Carpets" }
] }
]
});
$("#treeview").kendoTreeView({
dragAndDrop: true,
dataSource: inlineDefault,
dataTextField: "text"
});
//.kendoDraggable({
// container: $("#tree-pane"),
// hint: function () {
// return $("#treeview").clone();
// },
// dragstart: draggableOnDragStart
//});
$("#treeview").data("kendoTreeView").bind("dragstart", function (e) {
if ($(e.sourceNode).parentsUntil(".k-treeview", ".k-item").length == 0) {
e.preventDefault();
}
});
/*$("#treeview").data("kendoTreeView").bind("drop", function (e) {
e.preventDefault();
var copy = this.dataItem(e.sourceNode).toJSON();
if (e.dropPosition == "over") {
//var item = getObjects(nucleusTreeJsonData, 'text', e.sourceNode.textContent);
$("#listView").data("kendoListView").add(copy);
}
});*/
$('ul.k-group.k-treeview-lines div').children().css('font-weight', 'bold').find('div').css('font-weight', 'normal');
I'm not having much luck with it. Please take a look at my fiddle. Any suggestions would be greatly appreciated
http://jsfiddle.net/OhenewaDotNet/JQBZN/16/

I know this is an old question but I had it, too, so I went ahead and figured it out using this fiddle.
http://jsfiddle.net/JQBZN/74/
This is really really basic and is probably architected awfully but I think it at least demonstrates the key point(s):
$("#treeview").kendoTreeView({
dragAndDrop: true,
dataSource: inlineDefault,
dataTextField: "text",
drag: function (e) {
/* Manually set the status class. */
if (!$("#treeview").data('kendoTreeView').dataItem(e.sourceNode).hasChildren && $.contains($('#favorites-window')[0], e.dropTarget)) {
e.setStatusClass('k-add');
} else {
e.setStatusClass('k-denied');
}
},
drop: function (e) {
if (e.valid) {
/* Do your adding here or do it in a drop function elsewhere if you want the receiver to dictate. */
}
e.preventDefault();
}
});

If the KendoUI tool set isn't doing what you want it to do, you may find it easier to do what you want to do with jQuery UI. They're both implementing the same jQuery core library.
If you go with jQuery UI, it's simply a matter of binding 'draggable' to the element you want to drag, and 'droppable' to your targets. From there, you can wire up handlers to do pretty much anything you want.
I've set up a simple jsFiddle that demonstrates how this would work:
http://jsfiddle.net/e2fZk/23/
The jQuery code is really simple:
$(".draggable").draggable();
$(".container").droppable({
drop: function (event, ui) {
var $target = $(this);
var $source = ui.draggable;
var newUrl = $source.find("input").val();
alert("dropped on " + $target.attr("id") + ", setting URL to " + newUrl);
$target.find("#imageDiv").html("<img id='myImage' />")
.find("#myImage").attr("src", newUrl);
}
});
The API documentation is here:
Draggable
Droppable

Related

How add menu button to ag-Grid row?

I'm using ag-Grid Enterprise Vue.
I see in the docs how to enable a "context menu" that is available by right-clicking any individual cell.
I instead would love to have a special column (pinned to the right) that has a button (maybe looking like ⚙ or ...) that opens a menu upon left-click.
How could I go about enabling this? I have found no examples in the docs.
Ag-grid Cell containing menu button is a similar question but has no answer.
From this comment on this ag-grid-enterprise issue, I was able to fork the example, and I think it will work for my situation.
The relevant code is:
var gridOptions = {
columnDefs: columnDefs,
enableRangeSelection: true,
getContextMenuItems: getContextMenuItems,
allowContextMenuWithControlKey: true,
onCellClicked: params => {
console.log(params);
if(params.column.colDef.field === '...'){
params.api.contextMenuFactory.showMenu(params.node, params.column, params.value, params.event)
}
},
onCellContextMenu: params => {
params.api.contextMenuFactory.hideActiveMenu()
}
};
function getContextMenuItems(params) {
console.log('getContextMenuItems', params);
const node = params.node;
console.log('node.id, node.rowIndex, node.data', node.id, node.rowIndex, node.data);
var result = [
{
name: `Alert 'Row ${node.rowIndex + 1}'`,
action: function() {
window.alert(`Row ${node.rowIndex + 1}`);
},
cssClasses: ['redFont', 'bold']
},
'separator',
{
name: 'Checked',
checked: true,
action: function() {
console.log('Checked Selected');
},
icon: '<img src="../images/skills/mac.png"/>'
},
'copy' // built in copy item
];
return result;
}

Vuetify TreeView + Drag and drop

I am trying to implement drag and drop on Vuetify Treeview and data table. It seems like it is not supported fully but a workaround is described in this thread. The workaround is however not complete. Perhaps the community would benefit if someone created a codepen or similar on this?
What confuses me is that the component DragDropSlot.vue is created but "drag-drop-slot" is used in the code. Also there is a "_.cloneDeep(this.tree)" call where _ is not defined. I assume it should be replaced by something. When I comment that out drag and drop does still not work. Probably missed something more like defining data. Not sure of correct data types. It seems to be based on react which I have not worked with. Have just started to learn vue and vuetify.
I'm open for any suggestion for how to solve this.
All the best
I use V-Treeview with Vue.Draggable (https://github.com/SortableJS/Vue.Draggable).
I use direct link.
<script src="//cdn.jsdelivr.net/npm/sortablejs#1.8.4/Sortable.min.js"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0 vuedraggable.umd.min.js"/>
<v-treeview
:active.sync="active"
:items="users"
:search="search"
item-key="Id"
item-text="UserName"
item-children="Children"
:open.sync="open"
activatable
color="warning"
dense
transition
return-object
>
<template v-slot:label="{ item }">
<draggable :list="users" group="node" :id="item.Id" :data-parent="item.ParentId" #start="checkStart" #end="checkEnd" >
<label>
<i class="fas fa-user mr-3" />
<span id="item.id" >{{item.UserName}}</span>
</label>
</draggable>
Also I add ParentId property to item tree model:
{
Id:1,
UserName: "John Doe",
ParentId: null,
Children:[{Id:2, ParentId: 1,...}]
}
Then I use start and end events where I search parent start node from I drag the item and parent end node where I drop the item. When parent is null the item is a root.
new Vue({
el: '#app',
vuetify: new Vuetify(),
components: {
vuedraggable
},
data() {
return {
active: [],
open: [],
users: [],
selectedItems: [],
}
},
mounted: function () {
this.fetchUsers();
},
methods: {
findTreeItem: function (items, id) {
if (!items) {
return;
}
for (var i = 0; i < items.length; i++) {
var item = items[i];
// Test current object
if (item.Id === id) {
return item;
}
// Test children recursively
const child = this.findTreeItem(item.Children, id);
if (child) {
return child;
}
}
},
checkStart: function (evt) {
var self = this;
self.active = [];
self.active.push(self.findTreeItem(self.users, evt.from.id))
},
checkEnd: function (evt) {
var self = this;
var itemSelected = self.active[0];
var fromParent = itemSelected.ParentId ? self.findTreeItem(self.users, itemSelected.ParentId) : null;
var toParent = self.findTreeItem(self.users, evt.to.id);
var objFrom = fromParent ? fromParent.Children : self.users;
objFrom.splice(objFrom.indexOf(itemSelected), 1);
if (toParent.Id === itemSelected.Id) {
itemSelected.ParentId = null;
self.users.push(itemSelected);
}
else {
itemSelected.ParentId = toParent.Id;
toParent.Children.push(itemSelected);
}
self.saveUser(itemSelected);
// self.active = [];
return false;
},
fetchUsers: function () {
//load from api
},
saveUser: function (user) {
//save
},
},
computed: {
selected() {
if (!this.active.length) return undefined
return this.active[0];
},
}
})
Hope I help you.
IngD.
After some additional work I ended up with implementing Drag and Drop on top of vuetify tree view and data table using this library:
https://www.vuetoolbox.com/projects/vue-drag-drop
At first I looked at draggable and similar but realized it was always based on that you move an element from position A to position B. I needed more control. For example I wanted the element to disappear when dropping on some drop zones.
found this component.
https://vuejsexamples.com/vuetify-draggable-v-treeview-component/
I didn't try it myself (because it has too few options), but it looks working well in demo.
Anyways, just to try

Is it possible to have own custom Context Menu in ag-Grid-community

Can't find the exact answer.
If i decide to opt-in for vanilla JavaScript (non-Angular & Co) ag-Grid-community edition, can i have easy to add my own custom context menu an other custom extensions?
As i seen their docs, context menu is only enterprise level feature.
I seen some treads that there is some caveats, but i personally did not dig deeper.
In general, how easy is to implement self-built features in ag-Grid-community. Or it is better to write own grid?
We have a custom context menu component in our Angular project with ag-grid community, so it's definitely possible.
How it works:
We define all grid columns in templates. If you want a context menu, you put an empty column into the column set and put a special directive on it. The directive accepts a context menu template, which is passed into a custom cellRendererFramework (a menu trigger button, basically). The directive also configures the column to ensure consistent look across grid instances.
This might be not what you've been looking for if you require for menu to open with right mouse click anywhere in a row, but I suppose it shouldn't be that hard to trigger the menu from a different event (check out ag-grid events, there might something suitable).
The snippets below should be straightforward to adapt for your framework of choice. Given you opted into vanilla JS, you'll have to use regular functions to do the same, something like this:
const grid = withContextMenu(new Grid(element, gridOptions), menuOptions).
Here's an example of how we use it:
<ag-grid-angular>
<ag-grid-column headerName='ID' field='id'></ag-grid-column>
<ag-grid-column [contextMenu]='menu'>
<mat-menu #menu='matMenu'>
<ng-template matMenuContent let-item='data'>
<button mat-menu-item (click)='restoreSnapshot(item.id)'>Restore From Snapshot</button>
<a mat-menu-item [routerLink]='[item.id, "remove"]'>Remove</a>
</ng-template>
</mat-menu>
</ag-grid-column>
</ag-grid-angular>
The directive that applies the menu:
const WIDTH = 42;
export const CONTEXT_MENU_COLID = 'context-menu';
#Directive({
selector: '[agGridContextMenu]'
})
export class AgGridContextMenuDirective implements AfterViewInit {
constructor(private gridComponent: AgGridAngular) {}
#Input()
agGridContextMenu!: ElementRef<MatMenu>;
ngAfterViewInit() {
if (!this.agGridContextMenu) return;
setTimeout(() => {
this.gridComponent.api.setColumnDefs([
...this.gridComponent.columnDefs,
{
colId: CONTEXT_MENU_COLID,
cellRendererFramework: CellRendererContextMenuComponent,
width: WIDTH,
maxWidth: WIDTH,
minWidth: WIDTH,
cellStyle: {padding: 0},
pinned: 'right',
resizable: false,
cellRendererParams: {
suppressHide: true,
contextMenu: {
menu: this.agGridContextMenu
}
}
}
]);
});
}
}
The cell renderer component:
#Component({
selector: 'cell-renderer-context-menu',
template: `
<ng-container *ngIf='params.data && params.colDef.cellRendererParams.contextMenu.menu'>
<button
type='button'
mat-icon-button
[matMenuTriggerFor]='params.colDef.cellRendererParams.contextMenu.menu'
[matMenuTriggerData]='{data: params.data}'
>
<mat-icon svgIcon='fas:ellipsis-v'></mat-icon>
</button>
</ng-container>
`,
styleUrls: ['./cell-renderer-context-menu.component.scss']
})
export class CellRendererContextMenuComponent implements ICellRendererAngularComp {
params!: ICellRendererParams;
agInit(params: ICellRendererParams) {
this.params = params;
}
refresh() {
return false;
}
}
A screenshot:
I followed this blogpost, using community edition ag-grid, and it worked! I was surprised because previously I had the experience that cell renderers didn't allow content outside of the cell boundaries to be shown, but somehow popper/tippy is getting around that (I think it adds itself to the top of the DOM with this section of code appendTo: document.body).
https://blog.ag-grid.com/creating-popups-in-ag-grid/
basically, in my javascript CellRenderer:
class MyCellRenderer{
// https://www.ag-grid.com/javascript-data-grid/component-cell-renderer/
init(e){
this.isOpen = false;
this.container = document.createElement("span");
let menubutton = document.createElement("button");
menubutton.innerHTML="&#x1F80B"; //downward arrow
this.tippyInstance = tippy(menubutton);
this.tippyInstance.disable();
this.container.appendChild(menubutton);
menubutton.addEventListener('click', that.togglePopup.bind(this));
}
getGui() {
return this.container;
}
togglePopup() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.configureTippyInstance();
this.eMenu = this.createMenuComponent();
this.tippyInstance.setContent(this.eMenu);
} else {
this.tippyInstance.unmount();
}
}
configureTippyInstance() {
this.tippyInstance.enable();
this.tippyInstance.show();
this.tippyInstance.setProps({
trigger: 'manual',
placement: 'bottom-start',
arrow: false,
interactive: true,
appendTo: document.body,
hideOnClick: true,
onShow: (instance) => {
tippy.hideAll({ exclude: instance });
},
onClickOutside: (instance, event) => {
this.isOpen = false;
instance.unmount();
},
});
}
createMenuComponent() {
let menu = document.createElement('div');
menu.classList.add('menu-container');
let options = {};
options['Delete Row'] = this.menuItemClickHandler.bind(this);
options['Popup an Alert!'] = function(){alert("hello!");};
options['Popup an Alert 2!'] = this.menuItemClickHandler.bind(this);
for (const [key, value] of Object.entries(options)) {
let item = document.createElement('div');
item.classList.add('menu-item');
item.setAttribute('data-action', key.toLowerCase());
item.classList.add('hover_changes_color');
item.innerText = `${key}`; // string formatting example
item.addEventListener('click', value);
menu.appendChild(item);
}
return menu;
}
menuItemClickHandler(event) {
this.togglePopup();
const action = event.target.dataset.action;
if (action === 'delete row') {
this.params.api.applyTransaction({ remove: [this.params.data] });
}
if (action === 'popup an alert 2!') {
alert("2");
}
}
}
and in styles.css:
.hover_changes_color:hover {
background-color: dimgrey;
cursor: pointer;
}

kendo-ui autocomplete extend

I'm trying to extend the kendo-ui autocomplete control: I want the search start when te user hit enter, so basically I've to check the user input on keydown event.
I've tried to catch the keydown event with this code:
(function($) {
ui = kendo.ui,
Widget = ui.Widget
var ClienteText = ui.AutoComplete.extend({
init: function(element,options) {
var that=this;
ui.AutoComplete.fn.init.call(this, element, options);
$(this).bind('keydown',function(e){ console.log(1,e); });
$(element).bind('keydown',function(e){ console.log(2,e); });
},
options: {
[...list of my options...]
},
_keydown: function(e) {
console.log(3,e);
kendo.ui.AutoComplete.fn._keydown(e);
}
});
ui.plugin(ClienteText);
})(jQuery);
None of the binded events gets called, only the _keydown, and then I'm doing something wrong and cannot call the autocomplete "normal" keydown event.
I've seen a lot of examples that extend the base widget and then create a composite widget, but I'm not interested in doing that, I only want to add a functionality to an existing widget.
Can someone show me what I'm doing wrong?
Thank you!
What about avoiding the extend and take advantage of build in options and methods on the existing control : http://jsfiddle.net/vojtiik/Vttyq/1/
//create AutoComplete UI component
var complete = $("#countries").kendoAutoComplete({
dataSource: data,
filter: "startswith",
placeholder: "Select country...",
separator: ", ",
minLength: 50 // this is to be longer than your longest char
}).data("kendoAutoComplete");
$("#countries").keypress(function (e) {
if (e.which == 13) {
complete.options.minLength = 1; // allow search
complete.search($("#countries").val());
complete.options.minLength = 50; // stop the search again
}
});
This code actually work:
(function($) {
ui = kendo.ui,
ClienteText = ui.AutoComplete.extend({
init: function(element,options) {
ui.AutoComplete.fn.init.call(this, element, options);
$(element).bind('keydown',function(e){
var kcontrol=$(this).data('kendoClienteText');
if (e.which === 13) {
kcontrol.setDataSource(datasource_clientes);
kcontrol.search($(this).val());
} else {
kcontrol.setDataSource(null);
}
});
},
options: {
name: 'ClienteText',
}
});
ui.plugin(ClienteText);
})(jQuery);
but I don't know if it's the correct way to do it.

Select2 with AJAX and Initial Local Data

So I'm trying to get the select2 plugin to work with a Backbone.js / CakePHP app. The idea is that this select2 holds email addresses for contacting people as tasks become completed, but the form is editable. What I want to do is (1) load / display all the already saved email addresses for the task being edited, and (2) I want to still have the select2 perform AJAX searches to list recognized emails.
I keep having this issue where I can either show initial data, OR have the AJAX search feature.
My current code for my select2 box is a Backbone.View, and it looks like:
define([
'backbone',
'jquery',
'jquery.select2'
],
function(Backbone, $, select2) {
var notificationSelector = Backbone.View.extend({
notifications: undefined,
events: {
'change' : 'select2ContactsChanged'
},
initialize: function(attrs) {
this.collection.on('add remove reset', this.render(), this);
this.select2ContactsChanged();
},
render: function() {
var contacts = ["abc#def.com", "joe#banana.com"];
$('.notification-selector').attr('value', contacts);
if(this.select2Control == undefined)
{
// Do Search() + query here
this.select2Control = this.$el.select2({
width: '200px',
placeholder: '#email',
tags: [],
minimumInputLength: 3,
// initSelection: function(element, callback) {
// return $.ajax({
// type: "GET",
// url: "/notifications/fetch/",
// dataType: 'json',
// data: { id: (element.val()) },
// success: function(data) {
// }
// }).done(function(data) {
// console.log(data);
// });
// },
});
}
else
{
// Do Search() + query here
this.select2Control = this.$el.select2({
width: '200px',
placeholder: '#email',
tags: [],
minimumInputLength: 3,
ajax: {
url: '/notifications/search/',
dataType: 'json',
data: function(term, page) {
return {
SearchTerm: term
};
},
results: function(data, page) {
return {
results: data
};
}
}
});
}
},
select2ContactsChanged: function() {
var contacts = this.select2Control.val().split(',');
this.collection.reset(contacts);
}
});
return notificationSelector;
});
I read a response by the creator of Select2 to someone else (https://github.com/ivaynberg/select2/issues/392) in which he says to use a 'custom query' to achieve what seems to be what I want. I'm having trouble finding relevant examples or making enough sense of the docs to figure out what he means.
Can anyone spot what I'm doing wrong / missing?
Thanks for your time!
EDIT
I forgot to mention -- the DOM element this is attached to is <input type="hidden" multiple="true" class="notification-selector select2-result-selectable"></input>
Ok, I finally figured out the solution.
I was misunderstanding $.ajax() -- I did not really think about it actually being an asynchronous call. My code to check for the data being returned from the call was running before the AJAX actually finished, so I was always getting undefined.
I assigned a variable to the AJAX call, and set "async: false", and it worked perfectly.
fetchSetNotifications: function() {
var addresses = $.ajax({
method: 'GET',
dataType: 'json',
context: $('#notifications'),
url: '/Notifications/fetch/',
async: false,
alert(addresses);
}
The jqXHR object I get in 'addresses' then contains the response data I want in the "responseText" attribute.