Dojo dnd (drag and drop) 1.7.2 - How to maintain a separate (non-dojo-dnd) list? - dojo

I'm using Dojo dnd version 1.7.2 and it's generally working really well. I'm happy.
My app maintains many arrays of items, and as the user drags and drops items around, I need to ensure that my arrays are updated to reflect the contents the user is seeing.
In order to accomplish this, I think I need to run some code around the time of Source.onDndDrop
If I use dojo.connect to set up a handler on my Source for onDndDrop or onDrop, my code seems to get called too late. That is, the source that's passed to the handler doesn't actually have the item in it any more.
This is a problem because I want to call source.getItem(nodes[0].id) to get at the actual data that's being dragged around so I can find it in my arrays and update those arrays to reflect the change the user is making.
Perhaps I'm going about this wrong; and there's a better way?

Ok, I found a good way to do this. A hint was found in this answer to a different question:
https://stackoverflow.com/a/1635554/573110
My successful sequence of calls is basically:
var source = new dojo.dnd.Source( element, creationParams );
var dropHandler = function(source,nodes,copy){
var o = source.getItem(nodes[0].id); // 0 is cool here because singular:true.
// party on o.data ...
this.oldDrop(source,nodes,copy);
}
source.oldDrop = source.onDrop;
source.onDrop = dropHandler;
This ensures that the new implementation of onDrop (dropHandler) is called right before the previously installed one.

Kind'a shooting a blank i guess, there are a few different implementations of the dndSource. But there are a some things one needs to know about the events / checkfunctions that are called during the mouseover / dnddrop.
One approach would be to setup checkAcceptance(source, nodes) for any target you may have. Then keep a reference of the nodes currently dragged. Gets tricky though, with multiple containers that has dynamic contents.
Setup your Source, whilst overriding the checkAcceptance and use a known, (perhaps global) variable to keep track.
var lastReference = null;
var target = dojo.dnd.Source(node, {
checkAcceptance(source, nodes) : function() {
// this is called when 'nodes' are attempted dropped - on mouseover
lastReference = source.getItem(nodes[0].id)
// returning boolean here will either green-light or deny your drop
// use fallback (default) behavior like so:
return this.inhertied(arguments);
}
});
Best approach might just be like this - you get both target and source plus nodes at hand, however you need to find out which is the right stack to look for the node in. I believe it is published at same time as the event (onDrop) youre allready using:
dojo.subscribe("/dnd/drop", function(source, nodes, copy, target) {
// figure out your source container id and target dropzone id
// do stuff with nodes
var itemId = nodes[0].id
}
Available mechanics/topics through dojo.subscribe and events are listed here
http://dojotoolkit.org/reference-guide/1.7/dojo/dnd.html#manager

Related

How can I create the resource string without a big string?

In After Effects scripts, if you want your script to be able to be docked in the program's workspace, the only way to do it as far as I know is to use a resource string like this:
var res = "Group{orientation:'column', alignment:['fill', 'fill'], alignChildren:['fill', 'fill'],\
group1: Group{orientation:'column', alignment:['fill', ''], alignChildren:['fill', ''],\
button1: Button{text: 'Button'},\
},\
}";
myPanel.grp = myPanel.add(res);
The above code creates a script UI with one button ("button1") inside a group ("group1").
I would like to know other ways to create the same resource string. Is it possible to make it using a JSON object and then stringifying it??
I know it can be done somehow, because I have inspected the Duik Bassel script that is dockable and, for example, adds elements like this:
var button1 = myPal.add( 'button' );
but I cannot understand how to do it myself.
TL;DR: I want to make a dockable scriptUI without writing a giant string all at once, but bit by bit, like a floating script.
UI container elements have an add() method which allows you to add other UI elements to them, and you can treat them as normal objects.
var grp = myPanel.add("group");
grp.orientation = "column";
grp.alignment = ['fill', 'fill'];
grp.alignChildren = ['fill', 'fill'];
var group1 = grp.add("group");
…
var button1 = group1.add("button");
button1.text = 'Button'
More details and examples here: https://extendscript.docsforadobe.dev/user-interface-tools/types-of-controls.html#containers
Also worth checking out https://scriptui.joonas.me/ which is a visual scriptUI interface builder. You have to do some work on the code it produces to get panels for AE, but it's not hard.
extendscript still uses a 20th century version of javaScript, which doesn't have JSON built-in, but I have successfully used a JSON polyfill with it.
I used json2.js to get structured data in and out of Illustrator, and it worked beautifully, but I can see there's now a json3.js which might be better for whatever reason. This stackoverflow question addresses the differences.
To load another .js file (such as a polyfill) into your script, you need to do something like
var scriptsFolder = (new File($.fileName)).parent; // hacky but effective
$.evalFile(scriptsFolder + "/lib/json2.js"); // load JSON polyfill
These file locations may differ in other Adobe apps. Not sure what it would be in AfterEffects. I seem to remember that InDesign has a different location for scripts. You can also hardcode the path, of course.
Good luck!

trying to expand out nodes to null parent

I'm using cytoscape.js and cytoscape.js-expand-collapse to create dynamic hierarchies in my graph structure. I would like to be able to dynamically create a collapsed(merged) node that could potentially be expanded out, removed or possibly re-merged with additional nodes. I am having trouble whenever I call nodes.move({parent:null}). They become detached from the graph and I cannot re-attach them to a new parentNode. If I call restore on them I will see errors saying they already exist in the graph.
merge and unmerge by itself works fine in a simple case of no existing collapsed nodes. However calling merge on something that already contains a compound collapsed node breaks things. I would get the resulting merged node without the children of the previously collapsed merge candidate
Update #1 I've realized my problem was returning the wrong moved nodes. Calling .move on a collection returns a new set of nodes. so the unmerge should return those instead.
Update #2 cytoscape-expand-collapse utility does some internal book-keeping during the parent node collapse/expand by tucking away node data in 'originalEnds' data prop on the meta edges. so, if I'm now altering the graph dynamically by moving nodes in/out of parents these originalEnds pointers get out of sync causing infinite loops. I am doing some additional node tracking of my own as a workaround.
function merge(nodes){
//if one of the nodes is a compound itself, first uncollapse, then merge
var collapsed = nodes.filter(function(c){return typeof c.data('collapsedChildren')!=='undefined';});
if (collapsed.length>0) {
// for now just assume collapsed is a length of 1
var unmerged = unmerge(collapsed); //unmerged should be now the former collapsed children
nodes = nodes.subtract(collapsed).union(unmerged.nodes());
}
var parentNode = cy.add({group:'nodes', data: {id: parentID}});
nodes.move({parent: parentID});
collapseApi.collapse(parentNode);
return parentNode;
}
function unmerge(parentNode){
collapseApi.expand(parentNode);
var children = parentNode.children();
var moved = children.move({parent:null});
//at this point children become "detached" from the graph
// .removed() returns true, however, calling restore() logs an error and they are still technically in the graph
parentNode.remove();
return moved;
}
ele.move() has a more convenient implementation in cytoscape>=3.4: The elements are modified in-place instead of creating replacement elements.
Old code that uses the returned collection from ele.move() will still work. New code can be simplified by not having to use the returned collections at all.

dijit/Tree is not updated when connected to a dojo/store/JsonRest

I have modified the dojo tutorial at http://dojotoolkit.org/documentation/tutorials/1.10/store_driven_tree/demo/demo.html to read from a JsonRest store.
The problem is that the tree display doesn't update when I click "Add new child to selected item" e.g. on the root element, although the update worked in the original tutorial.
I have compared what dojo/store/Memory (from the original tutorial) and dojo/store/JsonRest return after the "put" request:
Memory returns the id of the new object.
JsonRest ends with "return xhr(...)", so it returns a Deferred instead of the new id, which seems not not be understood by the Observable. I can make it work, if I change dojo/store/JsonRest.js to end with:
...
return xhr(...).then(function(data){
return data.id;
};
}
I hope there is a solution without modifying the dojo sources?!
Some more details follow:
This is the definition of my store instead of the original Memory store:
var governmentStore = new JsonRest({
target : "http://localhost:8080/test/gov",
getChildren : function(object) {
return this.query({
parent : object.id
});
}
});
var governmentStore = new Cache(governmentStore,new Memory({}));
(If I remove the Cache and use the JsonRest directly, even the modified JsonRest.js doesn't make the Tree update).
This is the reply from a PUT request to the json server:
{"name":"New Child", "id":0.7243958345}
Please help to allow a dijit/Tree to react on changes of the underlying JsonRest store without messing around with the dojo sources.
Thank you
Dominic
Try wrapping your JsonRest store with an Observable wrapper and seeing if that helps the tree update properly. Also make sure that the model of the tree is functioning properly as that is what should be handling when and where the tree updates by listening to the store.
var memStore = new Memory({});
var store = new Observable(memStore); //Use this store for your tree
var cacheStore = new Cache(governmentStore,memStore);
The idea here is that when you do a PUT, you should be putting into the cacheStore and not the governmentStore. The Cache will do a PUT on the governmentStore but also update the memStore when the PUT is complete which should then trigger the notify in the Observable and pass that information along to the tree.
Using jquery instead of dojo was the solution. I found that I could solve in a few hours of learning jquery all problems that occurred when using dojo. This is mostly due to the quality of the documentation of both libraries and also because dojo seems to have too many bugs to react on new bug reports.

Dynamically add regions to Marionette layout

I have a layout, but cannot define all of its regions in advance because they are not known.
So later on an ItemView is created and I'd like to create a new region in the layout using the view's ID as the region's name so I can then say:
layout.dynamicRegionName.show(newItemView);
But there is cyclic dependency here.
I haven't rendered the view yet, so I cannot make a reference to its DOM element to be used in the layout's call to .addRegion()
I cannot render it, precisely because I want it to get attached to the DOM tree through the dynamically added region by calling its .show()
#DerickBailey In the Marionette.Layout docs in github I believe there is an error in the example that has: layout.show(new MenuView());
but technically this is close to what we'd need here i.e. to be able to do:
layout.addRegion(VAR_WITH_NEW_REGION_NAME, aViewInstance);
and have this add a new Region into the layout rendering inside it directly the view instance.
Am I missing some other obvious way to achieve this? Is that a known missing functionality? Is there a reason not to have it?
I'm aware of this previous Q: "Dynamically add/remove regions to a layout"
but don't see any clear/definite answer to it.
Marionette v1.0 (v1.0.2 is latest, right now) supports dynamic regions in Layouts.
var MyLayout = Marionette.Layout.extend({
template: "#some-template"
});
var layout = new MyLayout();
layout.render();
layout.addRegion("someRegion", "#some-element");
layout.someRegion.show(new MyView());
In one of my projects, I faced a similar issue. I needed to create a form dynamically, i.e the form would contain different field views that could not be determined prior runtime. I needed the fields to be Marionette views because they had pretty complicated behaviour.
The way I have done it in Marionette 1.4 in CoffeeScript:
class Module.AdditionalOptionsLayout extends Marionette.Layout
tagName: 'form'
initialize: (options = {}) ->
#_fieldViews = options.fieldViews || []
onRender: ->
#_showFields #_fieldViews
_showFields: (fieldViews) ->
fieldViews.forEach (fieldView) => #_addRegion().show fieldView
_addRegion: ->
regionClass = _.uniqueId('field-region__')
#$el.append $("<div class=\"#{regionClass}\"></div>")
#addRegion regionClass, '.' + regionClass
Please, let me know if it needs further explanation or I can clarify this in JS. I am also aware that it is a late answer, however, hope somebody could find it still useful. Also, note - the answer is relevant only for Marionette 1.x

wxWidgets Sizer does not automatically layout

Here is my code:
cLicensePanel::cLicensePanel( wxWindow * parent )
: wxPanel(parent,-1,wxPoint(0,0),wxSize(500,500))
{
wxBoxSizer * szrUserKey = new wxBoxSizer(wxHORIZONTAL);
myUserKeyCtrl = new wxTextCtrl(this,-1,L"");
szrUserKey->Add( new wxStaticText(this,-1,wxString(L"User's ComputerKey:")),
0,wxALL,10);
szrUserKey->Add( myUserKeyCtrl,0,wxALL,10);
szrUserKey->Add( new wxButton(this,IDC_Generate,L"Generate"),0,wxALL,10);
SetSizer( szrUserKey );
}
This is intended to produce this display, with the 3 widgets neatly arranged in a row.
However, what I actually get is all the widgets piled one on top of the other
Why?
I can force the correct display by adding an explicit call to Sizer::Layout()
cLicensePanel::cLicensePanel( wxWindow * parent )
: wxPanel(parent,-1,wxPoint(0,0),wxSize(500,500))
{
wxBoxSizer * szrUserKey = new wxBoxSizer(wxHORIZONTAL);
myUserKeyCtrl = new wxTextCtrl(this,-1,L"");
szrUserKey->Add( new wxStaticText(this,-1,wxString(L"User's Computer Key:")),0,wxALL,10);
szrUserKey->Add( myUserKeyCtrl,0,wxALL,10);
szrUserKey->Add( new wxButton(this,IDC_Generate,L"Generate"),0,wxALL,10);
szrUserKey->Layout();
SetSizer( szrUserKey );
}
I can live with this, though it seems to me that it should be unnecessary to call Layout. I also suspect that the reason the Layout call is neccessary might be an important clue to the problem that has me stumped.
The above is the behaviour when using wxWidgets v2.8.12. I am upgrading to v2.9.3.
In the new version the initial code, without the call to Layout(), shows the same problem with the widgets all piled on top of each other. If I add the call to layout() in v2.9.3 then I cannot see any of the widgets at all - the panel shows completely blank!
SetSizer() intentionally doesn't do the layout, it only happens when the window is resized. But it also so happens that a newly created window always gets a EVT_SIZE, soon after being shown, so by the time the user sees it, it's already laid out (and this is the reason SetSizer() doesn't do it: this would be wasteful as it would be redone very soon anyhow in 99% of cases).
In your case the panel doesn't get this size event for whatever reason. Perhaps -- looking at the rest of your code -- you can see what it is and avoid it. But if not, calling Layout() is a fine solution too. Except that you should call the same method on the panel itself, not on the sizer, i.e. do
cLicensePanel::cLicensePanel( wxWindow * parent )
{
...
SetSizer(szrUserKey);
Layout();
}