I would like to know if there is a way to move the node programmatically in dojo Dnd? The reason is I would like to revert the changes to the drag and drop when the web service call triggered a failed save on the database. Here is what I have so far.
In my code, the node Id seems to be unrecognized by the dojo.dnd.Container.DelItem. I cannot just use the selected item on the target because this is a asynchronous webservice function callback. So the user may be selecting another node on the container when this is called.
function OnMoveWSComplete(strResult) {
var resultObj = eval('(' + strResult + ')');
var sourceContainer = eval('(' + objResult.sourceContainerId + ')');
var targetContainer = eval('(' + objResult.targetContainerId + ')');
var targetNodes = targetContainer.getAllNodes();
for (var iNode = 0; iNode < targetNodes.length; iNode++) {
var currId = getLastIdFromStr(targetNodes[iNode].id);
if (currId == resultObj.Id) {
var targetN = targetNodes[iNode];
var Name = targetNodes[iNode].childNodes[0].nodeValue;
targetContainer.delItem(targetNodes[iNode].id);
var origData = { Id: resultObj.Id, Name: Name };
sourceContainer.insertNodes(true, origData);
break;
}
}
}
EDIT: Solution (Thanks Eugene Lazutkin) [2009/11/30]:
/**
* Move one node from one container to the other
*/
function moveNode(nodeId, sourceContainer, targetContainer) {
var node = dojo.byId(nodeId);
// Save the data
var saveData = sourceContainer.map[nodeId].data;
// Do the move
sourceContainer.parent.removeChild(node);
targetContainer.parent.appendChild(node);
// Sync the DOM object → map
sourceContainer.sync();
targetContainer.sync();
// Restore data for recreation of data
targetContainer.map[nodeId].data = saveData;
}
It looks like you assume that delItem removes physical nodes. Take a look at the documentation — probably you want to move nodes between containers instead of deleting them from the map. One simple way to do that just to move DOM nodes between containers, and call sync() on both containers.
Addition: Here is a super-simple pseudocode-like example:
function move(node, source, target){
// normalize node and/or node id
node = dojo.byId(node);
// move it physically from one parent to another
// (from target to source) adding to the end
target.parent.appenChild(node);
// now it is moved from source to target
// let's synchronize both dojo.dnd.Source's
source.sync();
target.sync();
}
Or something along these lines should work. The important pieces:
Move node from one parent to another using any DOM operations you deem appropriate. I used appendChild(), but you can use insertBefore(), or anything else.
Synchronize both sources involved after the move.
Obviously it works if both sources use nodes of the same type and structure. If not, you should do something more complex, e.g., move everything you need emulating a real DnD move by publishing topics described in the documentation.
I have this function which moves selected nodes by button click:
source.forInItems(dojo.hitch(this, function(item, id, map) {
if (dojo.hasClass(id, "dojoDndItemAnchor")) {
target.onDrop(source, [ dojo.byId(id) ], false);
dojo.removeClass(id, "dojoDndItemAnchor");
}
}));
onDrop() is an overridable method, which is called upon item drop, and by default calls to method onDropExternal(source, nodes, copy).
I am doing the same thing right now. I was able to solve by doing the following.
Set the dnd/Source autoSync property to true
<div data-dojo-type="dojo.dnd.Source" accept="widget" class="source" data-dojo-props="autoSync: true">
After dragging it looses the dndtype so I had to re-add it using client side code. Also I remove the dojoDndItemAnchor class after drop.
$(node).removeClass('dojoDndItemAnchor').attr('dndtype', 'widget');
Related
We have common functionality across multiple pages: Cancel/Save/Back/Next (buttons).
I added shortcut keys via app-level static JavaScript file: Alt-C/S/B/N (using common CSS on the buttons & jQuery binds).
Problem: when using shortcut key whilst editing a cell, the edits to the cell were not saved with Alt-S, Alt-N. (For same reason, no confirmation requested on Cancel/Back, since the grid did not see any changes.)
Much searching and reading of documentation did not reveal the answer...
After a huge amount of effort: inspecting of widget/ interactiveGrid/ model/ getCurrentView via the console & inspect-panes (partly because I didn't find/inspect the .interactiveGrid('getViews','grid).getColumns() for a long time), I found a solution, but I feel there must be a more elegant one. Advice sought! TIA.
Here is my solution. Note the namespace is lps, and I have aliased apex.jQuery as lps.$ prior to the code below. I include comments for my Oracle colleagues less experienced with JS, Apex, jQuery, etc.
The solution works for all 4 shortcut keys, and should work for any IG (possibly even without a static id on the containing region, although my test case has a static id). There is some supporting code after the main solution. All happening on page load via the static JS file.
The jQuery selectors must only bind to a single button on each page!
lps.$(window).on('keydown',function($evt) // Capture Alt + shortcut key in keydown event
{
// $evt is the jQuery simplified event
if( $evt.altKey ) // the alt-key is being pressed-and-held
{
var key = $evt.key.toUpperCase();
if( lps.shortcuts[key] )
{
if( lps.$($evt.target).is('input,textarea') ) // effect the onchange() JavaScript (more specifically, the IG version thereof)
{
// We've pressed a shortcut key combo, whilst editing a cell, so the IG-onchange-y event hasn't yet fired
var tgtEle = $evt.target,
newVal = tgtEle.value, // the value of the on-screen element, regardless of change-status
tgtId = tgtEle.id, // this tallies with an elementId exposed via the igCols below
activeFld;
// IG stuff - igId - get the IG overall id via an IG ancestor div of the target field, then losing the trailing _ig suffix
var igId = lps.$(tgtEle).parents('div.a-IG')[0].id.replace(/_ig$/,'')
igWidget= apex.region(igId).widget(),
igGrid = igWidget.interactiveGrid('getViews','grid'),
igModel = igGrid.model,
igView = igWidget.interactiveGrid('getCurrentView'),
igRecId = igView.getActiveRecordId(),
igCols = igGrid.getColumns(); // this provides meta information about the columns in the IG
for( var i=-1; ++i<igCols.length; ) // find the field name of this IG-column from target element id
{
if( igCols[i].elementId === tgtId )
{
activeFld = igCols[i].property;
break;
}
}
// Phew. Eventually setValue on the IG-cell so Apex knows the cell has changed without an onchange firing
igModel.setValue(igModel.getRecord(igRecId),activeFld,newVal);
}
$evt.originalEvent.preventDefault(); // we're consuming the event, so stop it doing some native browser trick we don't want
lps.$(lps.shortcuts[key].selector).select(); // select the to-be-clicked-element
lps.$(lps.shortcuts[key].selector).click(); // synthesise a click on the selector'd element
}
}
});
Supporting code to pre-establish what our 4 shortcut keys are to be:
lps.shortcuts = { // keys and their jQuery selectors
B : { selector:'button.nav-back' },
C : { selector:'button.nav-cancel' },
N : { selector:'button.nav-next' },
S : { selector:'button.nav-save' },
};
Code to add hover-title to the buttons:
// Advertise the shortcut with a 'title' attribute (appears as popup tip when hover over the element)
for( var key in lps.shortcuts ){
lps.$(lps.shortcuts[key].selector).attr('title','Alt+'+key);
}
I've create a dnd solution, with a source and target location. Right now the drag is bi-directional, I would like to have it be one-direction target to source. Then from the source add a image to each item (a delete icon), so that the users can then click the icon and send the record back to the correct target.
Part 1, I am trying to understand how to make the dnd one-directional and Part 2, how do I add an image to each item.
Thanks
There are events which handles this in the dnd module.
as for 'part1', see this http://dojotoolkit.org/reference-guide/1.8/dojo/dnd.html#id6 and set is'Source : false on the target which should only be a target.
and with 'part2' go to http://dojotoolkit.org/reference-guide/1.8/dojo/dnd.html#id8 and read about 'onDrop'. If overloading that function in your 'target-only' source, you will gain access to the nodes which is being dropped.
onDrop: function(source, nodes, copy) {
dojo.forEach(nodes, function(node) {
node.innerHTML += "<button title=Delete> X </button>";
});
}
This is what I got working.
dojo.connect(selectedInstructions, "onDndDrop", instructions.addDeleteButton);
addDeleteButton: function (source, nodes, copy, target) {
if (source != target) {
dojo.forEach(nodes, function(node) {
var instructionId = node.getAttribute("id");
var oImg = document.createElement("img");
oImg.setAttribute('src', 'images/delete.png');
oImg.setAttribute('alt', 'Remove');
oImg.setAttribute('class', 'remove_instruction');
oImg.setAttribute('onClick', "javascript:instructions.removeInstruction('" + instructionId + "')");
document.getElementById(instructionId).appendChild(oImg);
});
}
},
I then tried to get on to work, since connect is being depreciated, but I didn't appear to have much luck. I will have to come back to it at a later date, as I am on a time crunch right now to get this code out.
on(selectedInstructions, "onDrop", instructions.addDeleteButton);
aspect.after(selectedInstructions, "onDrop", instructions.addDeleteButton);
Wish the Dojo documentation was better. Thanks goodness for the community and it's support though.
Im creating a site who works with ajaxRequest, when I click a link, it will load using ajaxRequest. When I load for example user/login UserController actionLogin, I renderPartial the view with processOUtput to true so the js needed inside that view will be generated, however if I have clientScriptRegister inside that view with events, how can I avoid to generate the scriptRegistered twice or multiple depending on the ajaxRequest? I have tried Yii::app()->clientScript->isSCriptRegistered('scriptId') to check if the script is already registered but it seems that if you used ajaxRequest, the result is always false because it will only be true after the render is finished.
Controller code
if (Yii::app()->request->isAjaxRequest)
{
$this->renderPartial('view',array('model'=>$model),false,true);
}
View Code
if (!Yii::app()->clientScript->isScriptregistered("view-script"))
Yii::app()->clientScript->registerScript("view-script","
$('.link').live('click',function(){
alert('test');
})
");
If I request for the controller for the first time, it works perfectly (alert 1 time) but if I request again for that same controller without refreshing my page and just using ajaxRequest, the alert will output twice if you click it (because it keeps on generating eventhough you already registered it once)
This is the same if you have CActiveForm inside the view with jquery functionality.. the corescript yiiactiveform will be called everytime you renderPartial.
To avoid including core scripts twice
If your scripts have already been included through an earlier request, use this to avoid including them again:
// For jQuery core, Yii switches between the human-readable and minified
// versions based on DEBUG status; so make sure to catch both of them
Yii::app()->clientScript->scriptMap['jquery.js'] = false;
Yii::app()->clientScript->scriptMap['jquery.min.js'] = false;
If you have views that are being rendered both independently and as HTML fragments to be included with AJAX, you can wrap this inside if (Yii::app()->request->isAjaxRequest) to cover all bases.
To avoid including jQuery scripts twice (JS solution)
There's also the possibility of preventing scripts from being included twice on the client side. This is not directly supported and slightly more cumbersome, but in practice it works fine and it does not require you to know on the server side what's going on at the client side (i.e. which scripts have been already included).
The idea is to get the HTML from the server and simply strip out the <script> tags with regular expression replace. The important point is you can detect if jQuery core scripts and plugins have already been loaded (because they create $ or properties on it) and do this conditionally:
function stripExistingScripts(html) {
var map = {
"jquery.js": "$",
"jquery.min.js": "$",
"jquery-ui.min.js": "$.ui",
"jquery.yiiactiveform.js": "$.fn.yiiactiveform",
"jquery.yiigridview.js": "$.fn.yiiGridView",
"jquery.ba-bbq.js": "$.bbq"
};
for (var scriptName in map) {
var target = map[scriptName];
if (isDefined(target)) {
var regexp = new RegExp('<script.*src=".*' +
scriptName.replace('.', '\\.') +
'".*</script>', 'i');
html = html.replace(regexp, '');
}
}
return html;
}
There's a map of filenames and objects that will have been defined if the corresponding script has already been included; pass your incoming HTML through this function and it will check for and remove <script> tags that correspond to previously loaded scripts.
The helper function isDefined is this:
function isDefined(path) {
var target = window;
var parts = path.split('.');
while(parts.length) {
var branch = parts.shift();
if (typeof target[branch] === 'undefined') {
return false;
}
target = target[branch];
}
return true;
}
To avoid attaching event handlers twice
You can simply use a Javascript object to remember if you have already attached the handler; if yes, do not attach it again. For example (view code):
Yii::app()->clientScript->registerScript("view-script","
window.myCustomState = window.myCustomState || {}; // initialize if not exists
if (!window.myCustomState.liveClickHandlerAttached) {
window.myCustomState.liveClickHandlerAttached = true;
$('.link').live('click',function(){
alert('test');
})
}
");
The cleanest way is to override beforeAction(), to avoid any duplicated core script:
class Controller extends CController {
protected function beforeAction($action) {
if( Yii::app()->request->isAjaxRequest ) {
Yii::app()->clientScript->scriptMap['jquery.js'] = false;
Yii::app()->clientScript->scriptMap['jquery-2.0.0.js'] = false;
Yii::app()->clientScript->scriptMap['anything.js'] = false;
}
return parent::beforeAction($action);
}
}
Note that you have to put the exact js file name, without the path.
To avoid including script files twice, try this extension: http://www.yiiframework.com/extension/nlsclientscript/
To avoid attaching event handlers twice, see Jons answer: https://stackoverflow.com/a/10188538/729324
How can I receive all events attached to an element with dojo?
dojo.query('#mydiv') // which events does #mydiv has?
To get all events on a DOM element:
// Get my div
myDiv = dojo.byId("myDiv");
// Obtain all event-related attributes
var events = dojo.filter(
myDiv.attributes,
function(item) {
return item.name.substr(0, 2) == 'on';
}
);
// Execute first found event, just for fun
eval(events[0].value);
If you get myDiv using dojo.query, remember that dojo.query returns an array, so your element would be in myDiv[0].
This solution does not work with events attached with dojo.connect. There probably is a way to extract this info from Dojo inner workings, but you would have to delve into the source code to understand how.
Another option is that you explicitly manage all dojo.connect events with a global registry. You could use dojox.collections to make this easier. For example, creating a global registry whose keys will be the dom nodes, and values will be the handles returned by dojo.connect (these handles contain the dom node, the type of event and the function to execute):
// On startup
dojo.require(dojox.collections.Dictionary);
eventRegistry = new dojox.collections.Dictionary();
...
// Registering an event for dom node with id=myDiv
var handle1 = dojo.connect(dojo.byId("myDiv"), "onclick", null, "clickHandler");
// Check if event container (e.g. an array) for this dom node is already created
var domNode = handle1[0];
if (!eventRegistry.containsKey(domNode))
eventRegistry.add(domNode, new Array());
eventRegistry.item(domNode).push(handle1);
...
// Add another event later to myDiv, assume container (array) is already created
var handle2 = dojo.connect(dojo.byId("myDiv"), "onmouseover", null, "mouseHandler");
eventRegistry.item(domNode).push(handle2);
...
// Later get all events attached to myDiv, and print event names
allEvents = eventRegistry.item(domNode);
dojo.forEach(
allEvents,
function(item) {
console.log(item[1]);
// Item is the handler returned by dojo.connect, item[1] is the name of the event!
}
);
You can hide the annoying check to see if event container is already created by creating a subclass of dojox.collections.Dictionary with this check already incorporated. Create a js file with this path fakenmc/EventRegistry.js, and put it beside dojo, dojox, etc:
dojo.provide('fakenmc.EventRegistry');
dojo.require('dojox.collections.Dictionary');
dojo.declare('fakenmc.EventRegistry', dojox.collections.Dictionary, {
addEventToNode : function(djConnHandle) {
domNode = djConnHandle[0];
if (!this.containsKey(domNode))
this.add(domNode, new Array());
this.item(domNode).push(djConnHandle);
}
});
Using the above class you would have to dojo.require('fakenmc.EventRegistry') instead of 'dojox.collections.Dictionary', and would simply directly add the dojo connect handle without other checks:
dojo.provide('fakenmc.EventRegistry');
eventRegistry = new fakenmc.EventRegistry();
var handle = dojo.connect(dojo.byId("myDiv"), "onclick", null, "clickHandler");
eventRegistry.addEventToNode(handle);
...
// Get all events attached to node
var allEvents = eventRegistry.item(dojo.byId("myDiv"));
...
This code is not tested, but I think you get the idea.
If its only for debugging purpose. You can try dijit.byId("myId").onClick.toString(); in your firebug console and you can see the entire onclick code this works even if the function is anonymous you can view the content of anonymous content.
Using Dojo 1.3, after adding a child (i.e. folder or item) to a tree, is there a way to have it reflected immediately via refresh or some other method?
From the official Dojo manual
Updating a Tree
People often ask:
how do I update a tree (adding or
deleting items?)
You can't update the
tree directly, but rather you need to
update the model. Usually the model is
connected to a data store and in that
case you need to update the data
store. Thus, you need to use a data
store that allows updates (through
it's official API), like
dojo.data.ItemFileWriteStore.
how do I refresh a Tree from the
store?
This isn't supported. The store
needs to notify the tree of any
changes to the data. Currently this is
really only supported (out of the box)
by dojo.data.ItemFileWriteStore, as
setting up a client-server dojo.data
source where the server notifies the
client whenever the data has changed
is quite complicated, and beyond the
scope of dojo, which is a client-only
solution.
Say, if your model has the query `{type:'continent'} - meaning any items with this property are top-level-items, then the following will model extension will monitor changes and refresh the tree's view
var dataStore = new ItemFileWriteStore( { ... });
new Tree({
store: dataStore,
model: new ForestModel({
onNewItem: function(item, parentInfo){
if(this.store.getValue(item, 'type') == 'continent'){
this._requeryTop();
}
this.inherited(arguments);
}
}
});
This should in turn call childrenChanged in the tree and update it every time a new item is added.
See model reference
As an addition, if the item added is not a toplevel item, an immediate update should be doable with this statement. parent is the treenode which has had an item added to its children.
tree._collapseNode(parent);
parent.state = 'UNCHECKED';
tree._expandNode(parent);
A more or less 'standard' refresh of the tree can be achieved by following. Reason for it not being added to base implementation, i think is because it will break the linkage with DnD features on a tree
dojo.declare("My.Tree", [dijit.Tree], {
// Close the store? (So that the store will do a new fetch()).
reloadStoreOnRefresh : true,
update: function() {
this.model.store.clearOnClose = this.reloadStoreOnRefresh;
this.model.store.close();
// Completely delete every node from the dijit.Tree
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
// Destroy the widget
this.rootNode.destroyRecursive();
// Recreate the model, (with the model again)
this.model.constructor(this.model)
// Rebuild the tree
this.postMixInProperties();
this._load();
}
}
);
I've solved this WITHOUT needing the refresh.
_refreshNodeMapping: function (newNodeData) {
if(!this._itemNodesMap[newNodeData.identity]) return;
var nodeMapToRefresh = this._itemNodesMap[newNodeData.identity][0].item;
var domNode = this._itemNodesMap[newNodeData.identity][0].domNode;
//For every updated value, reset the old ones
for(var val in newNodeData)
{
nodeMapToRefresh[val] = newNodeData[val];
if(val == 'label')
{
domNode.innerHTML = newNodeData[val];
}
}
}