jsPlumb + Panzoom infinite droppable canvas - jquery-ui-droppable

I have created a codepen that uses jquery ui droppable(for drag/drop), jsPlumb (for flowcharting) and Panzoom (panning and zooming) to create a flowchart builder. You could drag the list items from the draggable container (1st column) to the flowchart (2nd column) and then connect the items using the dots to create a flowchart. The #flowchart is a Panzoom target with both pan and zoom enabled. This all works fine.
However, I would like to have the #flowchart div always span the whole area of the flowchart-wrapper i.e. the #flowchart should be an infinite canvas that supports panning, zooming and is a droppable container.
It should have the same effect as flowchart-builder-demo. The canvas there is infinite where you can drag and drop items (Questions, Actions, Outputs) from the right column.
Any pointers on how to achieve this (like the relevant events or multiple panzoom elements and/or css changes) would be greatly appreciated.
const BG_SRC_TGT = "#2C7BE5";
const HEX_SRC_ENDPOINT = BG_SRC_TGT;
const HEX_TGT_ENDPOINT = BG_SRC_TGT;
const HEX_ENDPOINT_HOVER = "#fd7e14";
const HEX_CONNECTOR = "#39afd1";
const HEX_CONNECTOR_HOVER = "#fd7e14";
const connectorPaintStyle = {
strokeWidth: 2,
stroke: HEX_CONNECTOR,
joinstyle: "round",
outlineStroke: "white",
outlineWidth: 1
},
connectorHoverStyle = {
strokeWidth: 3,
stroke: HEX_CONNECTOR_HOVER,
outlineWidth: 2,
outlineStroke: "white"
},
endpointHoverStyle = {
fill: HEX_ENDPOINT_HOVER,
stroke: HEX_ENDPOINT_HOVER
},
sourceEndpoint = {
endpoint: "Dot",
paintStyle: {
stroke: HEX_SRC_ENDPOINT,
fill: "transparent",
radius: 4,
strokeWidth: 3
},
isSource: true,
connector: ["Flowchart", { stub: [40, 60], gap: 8, cornerRadius: 5, alwaysRespectStubs: true }],
connectorStyle: connectorPaintStyle,
hoverPaintStyle: endpointHoverStyle,
connectorHoverStyle: connectorHoverStyle,
dragOptions: {},
overlays: [
["Label", {
location: [0.5, 1.5],
label: "Drag",
cssClass: "endpointSourceLabel",
visible: false
}]
]
},
targetEndpoint = {
endpoint: "Dot",
paintStyle: {
fill: HEX_TGT_ENDPOINT,
radius: 5
},
hoverPaintStyle: endpointHoverStyle,
maxConnections: -1,
dropOptions: { hoverClass: "hover", activeClass: "active" },
isTarget: true,
overlays: [
["Label", { location: [0.5, -0.5], label: "Drop", cssClass: "endpointTargetLabel", visible: false }]
]
};
const getUniqueId = () => Math.random().toString(36).substring(2, 8);
// Setup jquery ui draggable, droppable
$("li.list-group-item").draggable({
helper: "clone",
zIndex: 100,
scroll: false,
start: function (event, ui) {
var width = event.target.getBoundingClientRect().width;
$(ui.helper).css({
'width': Math.ceil(width)
});
}
});
$('#flowchart').droppable({
hoverClass: "drop-hover",
tolerance: "pointer",
drop: function (event, ui) {
var helper = $(ui.helper);
var fieldId = getUniqueId();
var offset = $(this).offset(),
x = event.pageX - offset.left,
y = event.pageY - offset.top;
helper.find('div.field').clone(false)
.animate({ 'min-height': '40px', width: '180px' })
.css({ position: 'absolute', left: x, top: y })
.attr('id', fieldId)
.appendTo($(this)).fadeIn('fast', function () {
var field = $("#" + fieldId);
jsPlumbInstance.draggable(field, {
containment: "parent",
scroll: true,
grid: [5, 5],
stop: function (event, ui) {
}
});
field.addClass('panzoom-exclude');
var bottomEndpoints = ["BottomCenter"];
var topEndPoints = ["TopCenter"];
addEndpoints(fieldId, bottomEndpoints, topEndPoints);
jsPlumbInstance.revalidate(fieldId);
});
}
});
const addEndpoints = (toId, sourceAnchors, targetAnchors) => {
for (var i = 0; i < sourceAnchors.length; i++) {
var sourceUUID = toId + sourceAnchors[i];
jsPlumbInstance.addEndpoint(toId, sourceEndpoint, { anchor: sourceAnchors[i], uuid: sourceUUID });
}
for (var j = 0; j < targetAnchors.length; j++) {
var targetUUID = toId + targetAnchors[j];
jsPlumbInstance.addEndpoint(toId, targetEndpoint, { anchor: targetAnchors[j], uuid: targetUUID });
}
$('.jtk-endpoint').addClass('panzoom-exclude');
}
// Setup jsPlumbInstance
var jsPlumbInstance = jsPlumb.getInstance({
DragOptions: { cursor: 'pointer', zIndex: 12000 },
ConnectionOverlays: [
["Arrow", { location: 1 }],
["Label", {
location: 0.1,
id: "label",
cssClass: "aLabel"
}]
],
Container: 'flowchart'
});
// Setup Panzoom
const elem = document.getElementById('flowchart');
const panzoom = Panzoom(elem, {
excludeClass: 'panzoom-exclude',
canvas: true
});
const parent = elem.parentElement;
parent.addEventListener('wheel', panzoom.zoomWithWheel);

I've just been working on the exact same issue and came across this as the only answer
Implementing pan and zoom in jsPlumb
The PanZoom used looks to be quite old - but the idea was the same, use the JQuery Draggable plugin for the movable elements, instead of the in-built JsPlumb one. This allows the elements to move out of bounds.
The below draggable function fixed it for me using the PanZoom library.
var that = this;
var currentScale = 1;
var element = $('.element');
element.draggable({
start: function (e) {
//we need current scale factor to adjust coordinates of dragging element
currentScale = that.panzoom.getScale();
$(this).css("cursor", "move");
that.panzoom.setOptions({ disablePan: true });
},
drag: function (e, ui) {
ui.position.left = ui.position.left / currentScale;
ui.position.top = ui.position.top / currentScale;
if ($(this).hasClass("jtk-connected")) {
that.jsPlumbInstance.repaintEverything();
}
},
stop: function (e, ui) {
var nodeId = $(this).attr('id');
that.jsPlumbInstance.repaintEverything();
$(this).css("cursor", "");
that.panzoom.setOptions({ disablePan: false });
}
});
I'm not sure if redrawing everything on drag is that efficient - so maybe just redraw both the connecting elements.

Related

Adding data to line chart (chart.js with Vue.js) results in 'too much recursion' error

I'm using chart.js with vue.js. I have a line-chart and I want to add data (later automatically by SSE). I modified another sample, but the error remains the same. It 'crashes' in the call to this.moonData.push (or this.testData.datasets[0].data.push). It must have to do with the ref() of moonData. When I use just the non-ref version, the push succeeds, but the chart isn't updated. BTW, pushing labels succeeds
I'm using chart.js#3.7.0, vue#3.2.29
In Firefox:
Uncaught InternalError: too much recursion
get reactivity.esm-bundler.js:406
toRaw reactivity.esm-bundler.js:927
key reactivity.esm-bundler.js:398
value helpers.segment.js:1554
key reactivity.esm-bundler.js:398
value helpers.segment.js:1554
key reactivity.esm-bundler.js:398
value helpers.segment.js:1554
in chrome:
runtime-core.esm-bundler.js?5c40:218 Uncaught RangeError: Maximum call stack size exceeded
at Object.get (reactivity.esm-bundler.js?a1e9:406:1)
at toRaw (reactivity.esm-bundler.js?a1e9:927:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
export default defineComponent({
// name: "PlanetChart",
setup() {
let moonData = ref<number[]>([]);
const testData = computed<ChartData<"line">>(() => ({
labels: ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"],
datasets: [
{
label: "Number of Moons",
data: moonData.value,
backgroundColor: "rgba(54,73,93,.5)",
borderColor: "#36495d",
borderWidth: 3,
},
{
label: "Planetary Mass (relative to the Sun x 10^-6)",
data: [0.166, 2.081, 3.003, 0.323, 954.792, 285.886, 43.662, 51.514],
backgroundColor: "rgba(71, 183,132,.5)",
borderColor: "#47b784",
borderWidth: 3,
},
],
}));
const options = ref<ChartOptions<"line">>({
elements: {
line: {
tension: 0,
fill: false,
},
},
scales: {
yAxes: {
ticks: {
padding: 25,
stepSize: 50,
},
},
},
});
return {
testData,
options,
moonData,
};
},
mounted() {
const ctx = document.getElementById("my-planet-chart") as HTMLCanvasElement;
console.log("Found context: ", ctx);
let c = new Chart(ctx, {
type: "line",
data: this.testData,
options: this.options,
});
console.log("Created chart: ", c);
},
methods: {
AddData() {
console.log("Appending data...");
this.moonData.push(Math.round(Math.random() * 1000));
console.log("moonData: ", this.moonData.length);
// this.testData.datasets[0].data.push(Math.round(Math.random() * 1000));
console.log("moonData: ", this.testData.datasets[0].data);
},
},
});
Any ideas?
I had exactly the same problem yesterday. Got it working using a shallowRef instead of a ref.

Get correct mouseover interaction in a ThreeJS VueJS app changing the window

I'm quite proud of what I've done: I have a menu which comprises 4 shapes. When you hover a shape, it's chaging color, growing and pushing the other shapes on top, while the rotation gets slower.
I read the ThreeJS docs and follow the advices of StackOverflow members.
I'm struggling with mouse interactions and window resizing: when I first open the browser, the mouseover doesn't seem to be called exactly when the mouse is over.
And when I resize the window, it's clearly messed up.
If anybody has a clue on what I'm doing wrong, thanks in advance :)
Here is my component:
<template>
<v-container>
<div #click="onClick" #mousemove="onMouseMove" id="menu3D" style="background-color: transparent; position: fixed; left: 20px; width:15%; height:100%;"></div>
<v-row class="text-center">
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Accueil
</h2>
<v-row justify="center">
<p>
THIS IS ONLY A TEST
</p>
</v-row>
</v-col>
</v-row>
</v-container>
</template>
<script>
import * as Three from 'three'
export default {
name: 'Home',
mounted() {
this.init();
},
methods: {
init: function() {
this.createScene();
this.createCamera();
this.userData.formes.forEach(x=>this.createShape(x))
this.addSpotlight(16777215/*'#fdffab'*/);
this.addAmbientLight();
this.animate();
window.addEventListener('resize', this.onResize())
},
onResize: function() {
let container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.camera.aspect = container.clientWidth / container.clientHeight;
this.camera.updateProjectionMatrix();
},
createScene: function() {
this.renderer = new Three.WebGLRenderer({
antialias: true,
alpha: true
});
let container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setClearColor(0xffffff,0);
container.appendChild(this.renderer.domElement);
},
createCamera: function() {
//let container = document.getElementById('container');
this.camera = new Three.PerspectiveCamera(50, 1.686275 /*container.clientWidth/container.clientHeight*/, 0.01, 1000);
this.camera.position.set(0, 5, 20);
this.camera.zoom = 1;
},
createShape: function(shape) {
let material = new Three.MeshStandardMaterial({
"color": '#0000ff'/*16777215*/,
"roughness": 1,
"metalness": 0.5,
"emissive": 0,
"depthFunc": 3,
"depthTest": true,
"depthWrite": true,
"stencilWrite": false,
"stencilWriteMask": 255,
"stencilFunc": 519,
"stencilRef": 0,
"stencilFuncMask": 255,
"stencilFail": 7680,
"stencilZFail": 7680,
"stencilZPass": 7680
})
switch (shape.nom) {
case "Box": {
this.geometry = new Three.BoxBufferGeometry(1.8,1.8,1.8)
break;
}
case "Sphere": {
this.geometry = new Three.SphereBufferGeometry(1,8,6,0,6.283185,0, 3.141593)
break;
}
case "Dodecahedron": {
this.geometry = new Three.DodecahedronBufferGeometry(1.2,0)
break;
}
case "Icosahedron": {
this.geometry = new Three.IcosahedronBufferGeometry(1.5,0)
break;
}
}
this.mesh = new Three.Mesh(this.geometry, material)
this.mesh.name = shape.nom
this.mesh.userData = shape.userData
this.mesh.receiveShadow = true
this.mesh.castShadow = true
this.mesh.position.set(0, shape.userData.position.y, 0)
this.scene.add(this.mesh)
},
addSpotlight: function(color) {
const light = new Three.SpotLight(color, 2, 1000)
light.position.set(0, 0, 30)
this.scene.add(light)
},
addAmbientLight: function() {
const light = new Three.AmbientLight('#fff', 0.5)
this.scene.add(light)
},
verifForme: function(e) {
let t = this
let elt = t.scene.getObjectByName(e);
t.intersects = t.raycaster.intersectObject(elt);
if (t.intersects.length !== 0) {
// if it's not in the array, we put it at the beginning
if (t.userData.souris.indexOf(e)<0) {
t.userData.souris.unshift(e);
console.log(t.userData.souris[0] + " survolé!");
}
if (t.userData.souris[0] == e) {
let obj = t.intersects[0].object;
obj.material.color.set('#'+elt.userData.couleurs[1]);
obj.scale.set(obj.scale.x<1.4?obj.scale.x+t.VITESSE_ZOOM:obj.scale.x,obj.scale.y<1.4?obj.scale.y+t.VITESSE_ZOOM:obj.scale.y,obj.scale.z<1.4?obj.scale.z+t.VITESSE_ZOOM:obj.scale.z);
obj.rotation.y += t.VITESSE_ROTATION/t.RALENTISSEMENT
t.replacer(obj,obj.userData.position.y+obj.userData.decalage)
}
else {
t.retrecir(e,elt);
}
}
else {
if (t.userData.souris.indexOf(e)>=0) {
t.userData.souris = t.userData.souris.filter(forme => forme != e);
}
t.retrecir(e,elt);
}
},
onClick: function ( event ) {
event.preventDefault();
if (this.userData.souris.length >0 ) { console.log(this.userData.souris[0] + " clicked!"); }
else {
console.log("click outside!")
}
},
onMouseMove: function(event){
let container = document.getElementById('menu3D');
this.mouse.x = ( event.clientX / container.clientWidth ) * 2 - 1;
this.mouse.y = - ( event.clientY / container.clientHeight ) * 2 + 1;
//console.log(JSON.stringify(this.mouse))
},
replacer: function(e,py) {
// next line to prevent shaking
if (Math.abs(e.position.y - py) < 0.05) { return true }
let rhesus = 10*this.VITESSE_ZOOM
if (this.userData.souris[0] != e.name) { rhesus *= 3 }
//console.log(e.name+': '+this.userData.souris[0]+' - '+rhesus)
if (e.position.y > py) { rhesus = -1 }
e.position.set(0,Math.trunc(10*e.position.y+rhesus)/10,0)
},
retrecir: function (n,e) {
// checking if the clicked element is on top
let dec = 0
let elt = this
if ((elt.userData.souris.length > 0) && (elt.userData.formes.map(x=>x.nom).indexOf(n)<elt.userData.formes.map(x=>x.nom).indexOf(elt.userData.souris[0]))) {
dec = Math.trunc(10*e.parent.getObjectByName(elt.userData.souris[0]).userData.decalage*2.1)/10;
}
e.material.color.set('#'+e.userData.couleurs[0]);
e.rotation.y += elt.VITESSE_ROTATION
e.scale.set(e.scale.x>1?e.scale.x-elt.VITESSE_ZOOM:e.scale.x,e.scale.y>1?e.scale.y-elt.VITESSE_ZOOM:e.scale.y,e.scale.z>1?e.scale.z-elt.VITESSE_ZOOM:e.scale.z);
let newY = e.userData.position.y+dec
if (e.position.y != newY) {
elt.replacer(e,newY)
}
},
animate: function() {
let elt = this
requestAnimationFrame(this.animate);
this.raycaster.setFromCamera(this.mouse, this.camera);
this.userData.formes.map(x=>x.nom).forEach(x=>elt.verifForme(x))
if (this.userData.souris.length >0 ) { document.body.style.cursor = "pointer"; }
else { document.body.style.cursor = "default"; }
this.camera.updateProjectionMatrix();
this.renderer.render(this.scene, this.camera);
}
},
data: () => ({
scene: new Three.Scene(),
camera: null,
renderer: Three.WebGLRenderer,
mesh: new Three.Mesh,
factor:0,
mouse : new Three.Vector2(1, 1),
raycaster : new Three.Raycaster(),
intersects : [],
VITESSE_ROTATION: 0.05,
VITESSE_ZOOM: 0.1,
RALENTISSEMENT: 3,
userData: {
"souris": [],
"formes": [
{
"nom": "Box",
"userData": {
"position": {
"x": 0,
"y": 7.8,
"z": 0
},
"couleurs": [
"aaaaaa",
"095256"
],
"decalage": 0.5
}
},
{
"nom": "Icosahedron",
"userData": {
"position": {
"x": 0,
"y": 5.5,
"z": 0
},
"couleurs": [
"aaaaaa",
"087F8C"
],
"decalage": 0.5
}
},
{
"nom": "Dodecahedron",
"userData": {
"position": {
"x": 0,
"y": 3.1,
"z": 0
},
"couleurs": [
"aaaaaa",
"5AAA95"
],
"decalage": 0.4
}
},
{
"nom": "Sphere",
"userData": {
"position": {
"x": 0,
"y": 1,
"z": 0
},
"couleurs": [
"aaaaaa",
"86A873"
],
"decalage": 0.2
}
}
]
}
}),
}
</script>
I investigated the problem using the code you provided and fixed it locally so I'm hoping it also works for you. The issues are the following:
A tiny hard to find typo in the attachment of the resize event: instead of window.addEventListener('resize', this.onResize()) you need to use window.addEventListener('resize', this.onResize); removing the () because you don't want to call the function at the time of the attachment, you want it called each time the event is triggered.
Due to the first issue, as the resize function wasn't getting called when you expected, I guess this is what led you to use a hard-coded value (1.686275) in the camera instantiation instead of the recommended formula container.clientWidth / container.clientHeight so you need to change that back to
createCamera: function () {
let container = document.getElementById('menu3D');
this.camera = new Three.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.01, 1000);
...
Also as the 3D container div is not in the root level of the HTML body due to VueJS requirements, in onMouseMove() you need to consume the offset coordinates instead of the client ones as follows:
onMouseMove: function (event) {
let container = document.getElementById('menu3D');
this.mouse.x = (event.offsetX / container.clientWidth) * 2 - 1;
this.mouse.y = - (event.offsetY / container.clientHeight) * 2 + 1;
...
I'm afraid the problem originates from the creation of your camera:
this.camera = new Three.PerspectiveCamera(50, 1.686275 /*container.clientWidth/container.clientHeight*/, 0.01, 1000);
Why are you using the magical aspect number 1.686275 instead of the actual width/height ratio like you do on resize? This is giving you a different behavior before and after resizing.
This is my best guess at first glance, although I presume there are other instances of hard coded “magic numbers” in your app that need to be re-calculated based on the screen’s width and height. I couldn't possibly read through the 300 lines of code you posted. You should consider isolating the problem to create a minimal working example and add it to your question via a code snippet so we can see your code in action.

ExtJs 4: How do I create a dynamic menu?

I have a menu system set up in a panel which needs to be dynamically created. I have created a mock static menu which the client likes but the menu categories and items will need to be loaded via JSON from a store.
Here is what I have for the first few menu items set statically:
Ext.define('SimpleSearch.view.FacetSDL' ,{
extend: 'Ext.panel.Panel',
alias : 'widget.facetsdl', //alias is referenced in MasterList.js
requires: ['SimpleSearch.store.SDLResults', 'FacetData' ],
title: 'Facet Search',
html: null,
frame: true,
layouts: 'fit',
items: [
{
id: 'group-menu',
title: 'Browse',
xtype: 'menu',
plain: true,
floating: false,
layouts: 'fit',
items: [
{
text: 'Security',
listeners:
{
click: function() {
var groupmenu = Ext.ComponentQuery.query('#group-menu')[0];
groupmenu.hide()
var securitymenu = Ext.ComponentQuery.query('#security-menu')[0];
securitymenu.setPosition(0,-groupmenu.getHeight(),false);
securitymenu.show()
}
},
menu: { // <-- submenu by nested config object
items: [
{
text: 'Classification',
listeners:
{
click: function() {
var groupmenu = Ext.ComponentQuery.query('#group-menu')[0];
groupmenu.hide()
var securitymenu = Ext.ComponentQuery.query('#security-menu')[0];
var classificationmenu = Ext.ComponentQuery.query('#classification-menu')[0];
classificationmenu.setPosition(0,-groupmenu.getHeight() - securitymenu.getHeight(),false);
classificationmenu.show()
}
I was thinking that maybe creating a class that loads all of the necessary data and then iterating through that class for the "items" field may be the way to go, but I am not sure if that will work.
You should look at using a Tree and TreeStore. Then make use of the ui:'menu' or viewConfig { ui: 'menu' } config properties to differentiate it from a regular tree. Then style it however your client wants.
This way you have all the mechanisms in place for free to handle the data dynamically and all your submenus, you'll just have to get a little creative on the CSS side of things.
I got it working like this:
var scrollMenu = Ext.create('Ext.menu.Menu');
for (var i = 0; i < store.getCount(); ++i){
var rec = store.getAt(i);
var item = new Ext.menu.Item({
text: rec.data.DISPLAY_FIELD,
value:rec.data.VALUE_FIELD,
icon: 'js/images/add.png',
handler: function(item){
myFunction(item.value); //Handle the item click
}
});
scrollMenu.add(item);
}
Then just add scrollMenu to your form or container. Hope this helps!
This menu is created dynamically with ExtJs, the data is loaded from Json.
See my demo with the code.
Demo Online:
https://fiddle.sencha.com/#view/editor&fiddle/2vcq
Json loaded:
https://api.myjson.com/bins/1d9tdd
Code ExtJs:
//Description: ExtJs - Create a dynamical menu from JSON
//Autor: Ronny Morán <ronney41#gmail.com>
Ext.application({
name : 'Fiddle',
launch : function() {
var formPanelFMBtn = Ext.create('Ext.form.Panel', {
bodyPadding: 2,
waitMsgTarget: true,
fieldDefaults: {
labelAlign: 'left',
labelWidth: 85,
msgTarget: 'side'
},
items: [
{
xtype: 'container',
layout: 'hbox',
items: [
]
}
]
});
var win = Ext.create('Ext.window.Window', {
title: 'EXTJS DYNAMICAL MENU FROM JSON',
modal: true,
width: 680,
closable: true,
layout: 'fit',
items: [formPanelFMBtn]
}).show();
//Consuming JSON from URL using method GET
Ext.Ajax.request({
url: 'https://api.myjson.com/bins/1d9tdd',
method: 'get',
timeout: 400000,
headers: { 'Content-Type': 'application/json' },
//params : Ext.JSON.encode(dataJsonRequest),
success: function(conn, response, options, eOpts) {
var result = Ext.JSON.decode(conn.responseText);
//passing JSON data in 'result'
viewMenuDinamical(formPanelFMBtn,result);
},
failure: function(conn, response, options, eOpts) {
//Ext.Msg.alert(titleAlerta,msgErrorGetFin);
}
});
}
});
//Generate dynamical menu with data from JSON
//Params: formPanelFMBtn - > Panel
// result - > Json data
function viewMenuDinamical(formPanelFMBtn,result){
var resultFinTarea = result;
var arrayCategoriaTareas = resultFinTarea.categoriaTareas;
var containerFinTarea = Ext.create('Ext.form.FieldSet', {
xtype: 'fieldset',
title: 'Menu:',
margins:'0 0 5 0',
flex:1,
layout: 'anchor',
//autoHeight: true,
autoScroll: true,
height: 200,
align: 'stretch',
items: [
]
});
var arrayMenu1 = [];
//LEVEL 1
for(var i = 0; i < arrayCategoriaTareas.length; i++)
{
var categoriaPadre = arrayCategoriaTareas[i];
var nombrePadre = categoriaPadre.nombreCategoria;
var hijosPadre = categoriaPadre.hijosCategoria;
var arrayMenu2 = [];
//LEVEL 2
for(var j = 0; j<hijosPadre.length; j++)
{
var categoriaHijo = hijosPadre[j];
var nombreHijo = categoriaHijo.nombreHijo;
var listaTareas = categoriaHijo.listaTareas;
var arrayMenu3 = [];
//LEVEL 3
for(var k = 0; k < listaTareas.length; k++)
{
var tarea = listaTareas[k];
var nombreTarea = tarea.nombreTarea;
var objFinLTres =
{
text: nombreTarea,
handler: function () {
alert("CLICK XD");
}
};
arrayMenu3.push(objFinLTres);
}
var menuLevel3 = Ext.create('Ext.menu.Menu', {
items: arrayMenu3
});
var objFinLDos;
if(arrayMenu3.length > 0)
{
objFinLDos = {
text: nombreHijo,
menu:menuLevel3
};
}
else
{
//without menu parameter If don't have children
objFinLDos = {
text: nombreHijo
};
}
arrayMenu2.push(objFinLDos);
}
var menuLevel2 = Ext.create('Ext.menu.Menu', {
items: arrayMenu2
});
var objFinLUno;
if(arrayMenu2.length > 0)
{
objFinLUno = {
text: nombrePadre,
menu:menuLevel2
};
}
else
{
//without menu parameter If don't have children
objFinLUno = {
text: nombrePadre
};
}
arrayMenu1.push(objFinLUno);
}
var mymenu = new Ext.menu.Menu({
items: arrayMenu1
});
containerFinTarea.add({
xtype: 'splitbutton',
text: 'Example xD',
menu: mymenu
});
formPanelFMBtn.add(containerFinTarea);
}

Animating dimensions in Sencha Touch 2

I'm trying to animate the height of a dataview, but it's currently just sliding the panel around the viewport instead of keeping it in place and changing it's height. The code is as follows:
Ext.Anim.run(el, 'slide', {
from: { height: height },
to: { height: newHeight },
out: false,
direction: 'up',
easing: 'ease-out',
duration: 1000
});
For instance, height=200, newHeight=100 will result in the dataview dropping immediately so that it's top is at 200px below the viewport, and then animating back to the top of the viewport.
How can I get it to change the height? Thanks.
Try using Ext.Animator.run instead:
Ext.Animator.run({
element: dataview.element,
duration: 500,
easing: 'ease-in',
preserveEndState: true,
from: {
height: dataview.element.getHeight()
},
to: {
height: 100
}
});
And within a full example:
Ext.application({
name: 'Sencha',
launch: function() {
var dataview = Ext.create('Ext.DataView', {
fullscreen: true,
style: 'background:red',
store: {
fields: ['text'],
data: [
{ text: 'one' },
{ text: 'two' },
{ text: 'three' }
]
},
itemTpl: '{text}'
});
Ext.Viewport.add({
xtype: 'button',
docked: 'top',
handler: function() {
Ext.Animator.run({
element: dataview.element,
duration: 500,
easing: 'ease-in',
preserveEndState: true,
to: {
height: 100
},
from: {
height: dataview.element.getHeight()
}
});
}
});
}
});
Since I can't add comments, I'll have to put this as a separate answer. I just wanted to add to what rdougan said and show how you can catch the animation end event. I find it's necessary in the above situation because Sencha Touch's component.getTop/Left/Height/Width() functions return incorrect values after an animation such as the one shown.
dataview.setHeight(dataview.element.getHeight()); // you may or may not need this
console.log('height before\t', dataview.getHeight());
var a = new Ext.fx.Animation({
element: dataview.element,
duration: 500,
easing: 'ease-in',
preserveEndState: true,
from: {
height: dataview.element.getHeight()
},
to: {
height: 100
}
});
a.on('animationend', function (animation, element, isInterrupted) {
console.log('height before\t', dataview.getHeight());
dataview.setHeight(dataview.element.getHeight());
console.log('height set\t', dataview.getHeight());
});
Ext.Animator.run(a);
I left in some logging so you can see just what I mean. This example was written against ST 2.1 RC2.
Here's a clean utility function you can use to accomplish this
function animatron (target, prop, duration, to, from, easing) {
// return if no target or prop
if (target == null || prop == null) { return; }
// defaults
if (duration == null) { duration = 250; }
if (to == null) { to = 0; }
if (from == null) { from = target.getHeight(); }
if (easing == null) { easing = 'ease-out'; }
// to property
var t = {};
t[prop] = to;
// from property
var f = {};
f[prop] = from;
// Animation Options
var opts = {
duration: duration,
easing: easing,
element: target.element,
from: f,
preserveEndState: true,
to: t
};
// Animation Object
var anime = new Ext.fx.Animation(opts);
// On animationend Event
anime.on('animationend', function (animation, element, isInterrupted) {
// Hide the target if the to is 0
if (to == 0 && (prop == 'height' || prop == 'width')) {
if (!isInterrupted) { target.hide(); }
}
// Update property if width or height
if (prop == 'height') { target.setHeight(to); }
if (prop == 'width') { target.setWidth(to); }
// Dispatch 'animated' event to target
target.fireEvent('animated', animation, element, to, from, isInterrupted);
});
// Show the target if it's hidden and to isn't 0
if (target.getHidden() == true && to != 0) { target.show(); }
// Run the animation
Ext.Animator.run(anime);
}
You can listen for the 'animated' event on the target element
animatron(dataview, 'height', 500, 0);
dataview.addListener('animated', function (animation, element, to, from, isInterrupted) {
console.log('animation ended');
console.log('interrupted: '+ isInterrupted);
});

Attaching an Event Listener

I've just finished creating a meticulously generated grid of icons (imageViews) and now I need to be able to do something with them. What I'm finding, though, is that the event listener I'm trying to bind isn't getting bound. Window loads, my icons are displayed nicely, but they aren't clickable.
Can anyone see what I'm missing? The code below is a fully functional (except for the part that doesn't function) file. You should be able to copy it into a test app and load it right up (may be iPhone-only at the moment).
Any insight would be much appreciated.
// this sets the background color of the master UIView (when there are no windows/tab groups on it)
Ti.UI.setBackgroundColor('#000');
//
// create base UI tab and root window
//
var win = Ti.UI.createWindow({
backgroundColor:'#fff',
layout: 'vertical',
navBarHidden: true,
});
// icon grid
var icons = [
{ image: '/images/ico_generic.png', label: 'Hospital Locations', url: 'http://google.com' },
{ image: '/images/ico_generic.png', label: 'Tobacco Free Campus', url: 'http:://robwilkerson.org' },
{ image: '/images/ico_generic.png', label: 'ER Wait Times', url: 'http://letmegooglethatforyou.com' },
{ image: '/images/ico_generic.png', label: 'Make a Donation', url: 'http://flickr.com/photos/robwilkerson' },
{ image: '/images/ico_generic.png', label: 'Condition Search', url: 'http://facebook.com' },
{ image: '/images/ico_generic.png', label: 'Video Library', url: 'http://google.com/reader' },
{ image: '/images/ico_generic.png', label: 'Financial Help', url: 'http://stackoverflow.com' },
{ image: '/images/ico_generic.png', label: 'Patient Forms', url: 'http://github.com' }
];
// put the grid in a scrollable view
var iconGrid = Ti.UI.createScrollView({
layout: 'vertical',
});
// incoming properties we want customizable
var cols = 3;
var icoW = 57;
var icoH = 57;
// Grid
var xSpacer = 10; // horizontal space b/t icons
var ySpacer = 10; // vertical space b/t icons
var rows = Math.ceil( icons.length / cols ); // how many rows?
// Container width = 1/3 of the viewport minus the icon widths and spacers
var containerW = Math.floor( ( Ti.Platform.displayCaps.platformWidth - ( xSpacer * ( cols + 1 ) ) ) / 3 );
// Container height = icon height + label spacer + label height
var containerH = icoH + ySpacer + 15;
// Row height = icon height + top spacer + bottom spacer + label spacer + 15 (label height)
var rowH = containerH + ( 2 * ySpacer );
// Incrementing values
var i = 0;
var viewHeight = 0;
for( var y = 0; y < rows; y++ ) {
var thisRow = Ti.UI.createView({
className: 'grid',
layout: 'horizontal',
height: rowH,
touchEnabled: false,
});
viewHeight += rowH;
for( var x = 0; x < cols && i < icons.length; x++ ) {
var container = Ti.UI.createView({
left: xSpacer,
height: containerH,
top: ySpacer,
width: containerW,
});
var icon = Ti.UI.createImageView({
left: ( containerW - icoW ) / 2,
height: icoH,
image: icons[i].image,
top: 0,
width: icoW,
});
var label = Ti.UI.createLabel({
// borderColor: '#00f',
font: { fontSize: 12 },
height: 15,
text: icons[i].label,
textAlign: 'center',
top: icoH + ySpacer,
width: containerW,
});
icon.addEventListener( 'click', function( e ) {
alert( 'Icon ' + i + ' was clicked' );d
});
container.add( icon );
container.add( label );
thisRow.add( container );
i++;
}
iconGrid.add( thisRow );
iconGrid.height = viewHeight;
}
win.add( iconGrid );
win.open();
You can also apply an event listener to the "view" itself. The reason being is, if you constantly add the same event listener to every single view, you'll cause the device's memory to become smaller and smaller, especially in cases where you'll have a larger data set.
My suggestion to you is this:
Add your own property to the imageView, like an "id" or something. So something like:
Ti.UI.createImageView({image: 'path/to/image.png', id: 'array_key'});
Once you've done that, you can add an event listener to the parent view, in this case your imageView.
view.addEventListener('click', function(e) {
alert(e.source.id + ' was clicked');
});
That way you have one event listener that can handle all the imageView events.
This one's on me. In my learning process, I went through a couple of different solutions to display a grid of icons. In one of the early iterations, I had to disable touch for the row (it was a tableView attempt). Several iterations later I got the display right, but disabling touch access on the row killed my ability to "click" the icons.
I was so far down the road that I didn't even realize that property was still in place until a new set of eyes pointed it out to me. Once I removed that property on thisRow, the event listeners got bound properly.
I am adding some line of code. What I have done is like created the grid of images and when you click, you will be able to that image.
{
"body": [
{
"type": "photo",
"order": 1,
"photos": [
{
"thumbnail": "http://www.flower.com/version_2.0/files/photos/thumbnails/745178756-_-1331130219.jpg",
"photo": "http://www.flower.com/version_2.0/files/photos/745178756-_-1331130219.jpg"
},
{
"thumbnail": "http://www.flower.com/version_2.0/files/photos/thumbnails/58062938-_-1337463040.jpg",
"photo": "http://www.flower.com/version_2.0/files/photos/58062938-_-1337463040.jpg"
},
{
"thumbnail": "http://www.flower.com/version_2.0/files/photos/thumbnails/1368715237-_-1337463149.jpg",
"photo": "http://www.flower.comversion_2.0/files/photos/1368715237-_-1337463149.jpg"
},
]
},
],
"status": true
}
It was response I was getting from the server.
Now for Making it is in grid and for clickable image, I am going to paste the code below. Note grid is done for 320 px width.
var xhr = Ti.Network.createHTTPClient({
onload : function(e) {
var response = JSON.parse(this.responseText);
var myObjectString = JSON.stringify(response);
Titanium.API.info('myObjectString--->: ' + myObjectString)
var myArray = response.body;
var objectArray = [];
var k = 5;
for (var i = 0; i < myArray[0].photos.length/5; i++) {
var l = 0+i*5; var m = 0 for (var j = l; j < k; j++) {
var thumb = Ti.UI.createImageView({
image:myArray[0].photos[j].thumbnail,
largeImage:myArray[0].photos[j].photo,
height:60,
tag:j,
width:60,
top:5*(i+1)+60*i,
left:3*(m+1)+60*m,
});
objectArray.push(thumb);
m++;
scroll.add(thumb);
thumb.addEventListener('click' ,function(e)
{
for(var i =0;i<objectArray.length;i++)
{
if(e.source.tag==objectArray[i].tag)
{
var LargeImageView = Ti.UI.createWindow({
backButtonTitle:'Image',
barColor:'#000',
backgroundColor: '#fff',
backgroundImage:'./Images/background.png',
url:'/More/DetailsImage.js',
image:objectArray[i].largeImage,
ImageArray:objectArray,
index:i,
});
Titanium.UI.currentTab.open(LargeImageView,{animated:true,modal:true});
break;
}
}
}); } l=k+5; k=k+5;
} } });