How to add context menu on Raphael element? - javascript

I am working with ExtJS 4 framework and also using Raphael.js in my main application panel.
First I was trying to prevent the regular context menu from showing, and I found out very easy solution.
raphael_element.node.oncontextmenu = function()
{
return false;
}
This is working great, but before returning false I want to add some code to show my own custom menu. Does anyone know how to do that?
I have something in html, that was thinking to use in the menu:
<ul id="rect_menu" class="contextMenu">
<li>Set aaa</li>
<li>Set xxx</li>
<li>Set ccc</li>
</ul>
As you can see, I have css class (code is not here, it irrelevant) for styling and id=rect_menu to show up when user right clicks on the i.e. Raphael rectangle object.
Can anyone show a way, maybe small demo that does not involve jquery? Thnaks

You can use Ext to display your custom HTML in a floating component:
var menu = Ext.create('Ext.Component', {
floating: true
,html: ['<ul id="rect_menu" class="contextMenu">',
'<li>Set aaa</li>',
'<li>Set xxx</li>',
'<li>Set ccc</li>',
'</ul>'].join('')
});
Ext.get(raphael_element.node).on({
contextmenu: function(e) {
menu.showAt(e.getXY());
e.preventDefault();
}
});
However, showing the menu is not the tricky part. The logic to have your menu hidden, either when the user click outside of it, or when the ESC key is pressed is a lot more involved. That's why I would use a regular Menu to let Ext do the heavy lifting for us.
var menu = Ext.create('Ext.menu.Menu', {
items: [{
text: 'Set aaa'
,handler: function() {
// logic for aaa
}
}, {
text: 'Set bbb'
,handler: function() {
// logic for bbb
}
}]
});
Ext.get(raphael_element.node).on({
contextmenu: function(e) {
menu.showAt(e.getXY());
e.preventDefault();
}
});
Now, if you're not interested in using all the menu system, with Ext menu items, etc., and you really want to render your own HTML, you can still leverage Ext menu to benefit from its hiding mechanics:
var menu = Ext.create('Ext.menu.Menu', {
// This will prevent the icon gutter from showing, and your component from
// being left padded
plain: true
,items: [{
xtype: 'component'
,html: ['<ul id="rect_menu" class="contextMenu">',
'<li>Set aaa</li>',
'<li>Set xxx</li>',
'<li>Set ccc</li>',
'</ul>'].join('')
}]
});
Ext.get(raphael_element.node).on({
contextmenu: function(e) {
menu.showAt(e.getXY());
e.preventDefault();
}
});
Here's a fiddle using that last example. It doesn't use Raphael, but from the moment you get a DOM element that won't bear any importance.
Edit Example with actual Raphael elements
I've updated my fiddle with actual Raphael elements. It does work exactly as intended.
What is cool is that the click event of the element totally respects the vectorial path bounds, meaning you can precisely decide where your menu will pop or not. What is less cool is that with, for example, the famous tiger, that would mean 240 event handlers... Kinda ouch performance-wise.... You can always add the handler to the SVG element instead of individual shape elements, but the menu will be bound to the whole drawing rectangle instead of individual parts of course.
// --- Creating a ball (see http://raphaeljs.com/ball.html)
Raphael.fn.ball = function (x, y, r, hue) {
hue = hue || 0;
return this.set(
this.ellipse(x, y + r - r / 5, r, r / 2).attr({fill: "rhsb(" + hue + ", 1, .25)-hsb(" + hue + ", 1, .25)", stroke: "none", opacity: 0}),
this.ellipse(x, y, r, r).attr({fill: "r(.5,.9)hsb(" + hue + ", 1, .75)-hsb(" + hue + ", .5, .25)", stroke: "none"}),
this.ellipse(x, y, r - r / 5, r - r / 20).attr({stroke: "none", fill: "r(.5,.1)#ccc-#ccc", opacity: 0})
);
};
var R = Raphael("holder"), x = 310, y = 180, r = 150;
var ball = R.ball(x, y, r, Math.random());
// --- Creatin a custom menu
var menu = Ext.create('Ext.menu.Menu', {
plain: true
,cls: 'myContextMenu'
,items: [{
xtype: 'component'
,html: ['<ul id="rect_menu" class="contextMenu">',
'<li>Set aaa</li>',
'<li>Set xxx</li>',
'<li>Set ccc</li>',
'</ul>'].join('')
}]
});
// --- Adding menu handler to all ball's elements
var contextMenuHandler = function(e) {
menu.showAt(e.getXY());
e.preventDefault();
};
Ext.each(ball, function(raphaelElement) {
Ext.fly(raphaelElement.node).on({
contextmenu: contextMenuHandler
});
});

This works good as well, and is under MIT license
https://swisnl.github.io/jQuery-contextMenu//
JsFiddle (example copied from link above)
$(function() {
$.contextMenu({
selector: '.context-menu-one',
callback: function(key, options) {
var m = "clicked: " + key;
window.console && console.log(m) || alert(m);
},
items: {
"edit": {name: "Edit", icon: "edit"},
"cut": {name: "Cut", icon: "cut"},
copy: {name: "Copy", icon: "copy"},
"paste": {name: "Paste", icon: "paste"},
"delete": {name: "Delete", icon: "delete"},
"sep1": "---------",
"quit": {name: "Quit", icon: function(){
return 'context-menu-icon context-menu-icon-quit';
}}
}
});
$('.context-menu-one').on('click', function(e){
console.log('clicked', this);
})
});

Related

Dynamically created elements not draggable

I have a simple block its element is dynamically added to DOM, I want the user to be able to create a block and it should be draggable using jsplumb library.
Unfortunately, now I can create element but their not draggable but if I add them manually to the dom, it's draggable.
Here is what I have so far
function addMovieButton() {
var newMovieBlockButton = $("<div class='movie-button w'>Button New<div class='ep' action='begin'></div><div>");
}
Here is plumb.js
jsPlumb.ready(function () {
// setup some defaults for jsPlumb.
var instance = jsPlumb.getInstance({
Endpoint: ["Dot", {radius: 5}],
Connector:"StateMachine",
HoverPaintStyle: {stroke: "#1e8151", strokeWidth: 2 },
ConnectionOverlays: [
[ "Arrow", {
location: 1,
id: "arrow",
length: 14,
foldback: 0.8
} ],
[ "Label", { label: "FOO", id: "label", cssClass: "aLabel" }]
],
Container: "canvas"
});
instance.registerConnectionType("basic", { anchor:"Continuous", connector:"StateMachine" });
window.jsp = instance;
var canvas = document.getElementById("canvas");
var windows = jsPlumb.getSelector(".statemachine-demo .w");
var windows_movie = jsPlumb.getSelector(".statemachine-demo .movie-block ");
// bind a click listener to each connection; the connection is deleted. you could of course
// just do this: jsPlumb.bind("click", jsPlumb.detach), but I wanted to make it clear what was
// happening.
instance.bind("click", function (c) {
instance.deleteConnection(c);
});
// bind a connection listener. note that the parameter passed to this function contains more than
// just the new connection - see the documentation for a full list of what is included in 'info'.
// this listener sets the connection's internal
// id as the label overlay's text.
instance.bind("connection", function (info) {
info.connection.getOverlay("label").setLabel(info.connection.id);
});
// bind a double click listener to "canvas"; add new node when this occurs.
jsPlumb.on(canvas, "dblclick", function(e) {
// newNode(e.offsetX, e.offsetY);
});
//
// initialise element as connection targets and source.
//
var initNode = function(el) {
// initialise draggable elements.
instance.draggable(el);
instance.makeSource(el, {
filter: ".ep",
anchor: "Continuous",
connectorStyle: { stroke: "#5c96bc", strokeWidth: 2, outlineStroke: "transparent", outlineWidth: 4 },
connectionType:"basic",
extract:{
"action":"the-action"
},
maxConnections: 6,
onMaxConnections: function (info, e) {
alert("Maximum connections (" + info.maxConnections + ") reached");
}
});
instance.makeTarget(el, {
dropOptions: { hoverClass: "dragHover" },
anchor: "Continuous",
allowLoopback: true
});
// this is not part of the core demo functionality; it is a means for the Toolkit edition's wrapped
// version of this demo to find out about new nodes being added.
//
instance.fire("jsPlumbDemoNodeAdded", el);
};
// suspend drawing and initialise.
instance.batch(function () {
for (var i = 0; i < windows.length; i++) {
initNode(windows[i], true);
console.log(windows[i]);
}
for (var j = 0; j < windows_movie.length; j++) {
initNode(windows_movie[j], true);
console.log(windows_movie[j]);
}
});
jsPlumb.fire("jsPlumbDemoLoaded", instance);
});
Here is live demo live demo
Here is plunker full source code
On the demo above just right click to add movie block for testing
Why does draggable not working for dynamically created elements?
here is a sample page I made a while ago when I first discovered 'jsplumb', it does exactly what you want so you might wanna use it or build on top of it.
Remember, indeed you should call the draggable method after the elements are added to the DOM, my example is so simple:
it doesn't need the jsplumb.fire
it doesn't need the .ready binding
it doesn't need the 'batch' processing offered by jsplumb
so you get to avoid problems like the scope of ready and other I'm still trying to master.

How to Change CSS of Custom Template JointJS Element?

I've been using JointJS for a while now and I'm trying to create an HTML template for my elements. So I've been using the tutorial but it didn't quit do it for me.
The thing I'm trying to accomplish is to adjust the color of the HTML element when an action has been performed, like double clicking the element. I did notice the way that the text is changed in the tutorial, but there is no example of changing any colors.
Edit
I've tried this to get a starting color on the element:
joint.shapes.html = {};
joint.shapes.html.OdinElement = joint.shapes.basic.Rect.extend({
defaults: joint.util.deepSupplement({
type: 'html.Element',
attrs: {
rect: { stroke: 'none', 'fill-opacity': 0 }
}
}, joint.shapes.basic.Rect.prototype.defaults)
});
// Create a custom view for that element that displays an HTML div above it.
// -------------------------------------------------------------------------
joint.shapes.html.OdinElementView = joint.dia.ElementView.extend({
template: [
'<div class="html-element">',
'<button class="delete">x</button>',
'<label></label>',
'<span></span>', '<br/>',
'</div>'
].join(''),
initialize: function() {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.$box = $(_.template(this.template)());
this.$box.find('.delete').on('click', _.bind(this.model.remove, this.model));
// Update the box position whenever the underlying model changes.
this.model.on('change', this.updateBox, this);
// Remove the box when the model gets removed from the graph.
this.model.on('remove', this.removeBox, this);
this.updateBox();
},
render: function() {
joint.dia.ElementView.prototype.render.apply(this, arguments);
this.paper.$el.prepend(this.$box);
this.updateBox();
return this;
},
updateBox: function() {
// Set the position and dimension of the box so that it covers the JointJS element.
var bbox = this.model.getBBox();
// Example of updating the HTML with a data stored in the cell model.
this.$box.find('label').text(this.model.get('label'));
this.$box.css({ width: bbox.width, height: bbox.height, left: bbox.x, top: bbox.y, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)', background: this.model.get('color')}); // I've tried to add it like a background
},
removeBox: function(evt) {
this.$box.remove();
}
});
//add a new element like this
new joint.shapes.html.OdinElement({
position: { x: 80, y: 80 },
size: { width: 200, height: 50 },
label: 'label',
color: '#ff0000'
});
I've also tried to set it like the text is set in the label, but I've no idea if there is a function for that.
Does anyone have any idea how to do this?
Thanks a lot!
Tim
I found the answer myself!
Apparently it is not allowed to create a different element in the html variable than the given Element. So I had to change the joint.shapes.html.OdinElement to joint.shapes.html.Element and the joint.shapes.html.OdinElementView to joint.shapes.html.ElementView
Now it works all fine :)
Thanks for your help!

How to give JointJS elements a remove tool?

In JointJS, links come with a handy responsive tool for removing links (when you hover over the link, an "x" appears, and clicking it removes the link). Elements, on the other hand, have a remove() method in the API, but don't have the UI "x" to expose that method to users.
Is there a straightforward way to give users the ability to delete elements in the UI?
In my project I define a custom shape - toolElement - that encapsulates this behaviour and then extend this with other custom shapes as required.
Full disclosure: This technique leans heavily on the jointjs code for links - I just adapted it :o)
Here is a jsfiddle showing it working:
http://jsfiddle.net/kj4bqczd/3/
The toolElement is defined like this:
joint.shapes.tm.toolElement = joint.shapes.basic.Generic.extend({
toolMarkup: ['<g class="element-tools">',
'<g class="element-tool-remove"><circle fill="red" r="11"/>',
'<path transform="scale(.8) translate(-16, -16)" d="M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z"/>',
'<title>Remove this element from the model</title>',
'</g>',
'</g>'].join(''),
defaults: joint.util.deepSupplement({
attrs: {
text: { 'font-weight': 400, 'font-size': 'small', fill: 'black', 'text-anchor': 'middle', 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle' },
},
}, joint.shapes.basic.Generic.prototype.defaults)
});
You can add more markup if you need other tools as well as the remove button.
The remove behaviour is encapsulated in a custom view:
joint.shapes.tm.ToolElementView = joint.dia.ElementView.extend({
initialize: function() {
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
},
render: function () {
joint.dia.ElementView.prototype.render.apply(this, arguments);
this.renderTools();
this.update();
return this;
},
renderTools: function () {
var toolMarkup = this.model.toolMarkup || this.model.get('toolMarkup');
if (toolMarkup) {
var nodes = V(toolMarkup);
V(this.el).append(nodes);
}
return this;
},
pointerclick: function (evt, x, y) {
this._dx = x;
this._dy = y;
this._action = '';
var className = evt.target.parentNode.getAttribute('class');
switch (className) {
case 'element-tool-remove':
this.model.remove();
return;
break;
default:
}
joint.dia.CellView.prototype.pointerclick.apply(this, arguments);
},
});
You can then extend these to make your custom shapes. In my project, I am doing data flow diagrams and here is the definition of the Process shape:
joint.shapes.tm.Process = joint.shapes.tm.toolElement.extend({
markup: '<g class="rotatable"><g class="scalable"><circle class="element-process"/><title class="tooltip"/></g><text/></g>',
defaults: joint.util.deepSupplement({
type: 'tm.Process',
attrs: {
'.element-process': { 'stroke-width': 1, r: 30, stroke: 'black', transform: 'translate(30, 30)' },
text: { ref: '.element-process'}
},
size: { width: 100, height: 100 }
}, joint.shapes.tm.toolElement.prototype.defaults)
});
and view:
joint.shapes.tm.ProcessView = joint.shapes.tm.ToolElementView;
I show and hide the tool markup, depending whether the element is highlighted using CSS. You could do the same when hovering (like the links do) if you like:
.element .element-tools {
display: none;
cursor: pointer
}
.element.highlighted .element-tools {
display: inline;
}
When rendered, it looks like this (note: in my case, I have another button in the tools, not just the remove - that is what the green chevron button is. I removed this from the code samples above to make them simpler):
When the element is not highlighted:
When it is highlighted:
I can then define other shapes really easily by extending toolElement. Here are the data flow diagram shapes for data stores:
and external actors:
Have a look at the HTML example on the JointJS website.
As you can see the elements have a close button there, so there's no need to complicate things by creating your own. Simply create a view for your element that contains the HTML code for the button, as well as the event handling. It's all in the source code of the example.
Note that the example doesn't provide you the CSS file for the HTML elements, but you also need it: http://resources.jointjs.com/tutorials/joint/tutorials/css/html-elements.css
A more native approach could be using the provided elementTools:
const view = element.findView(paper);
const removeButton = new joint.elementTools.Remove({
focusOpacity: 0.5,
rotate: true,
x: '50%',
y: '0%',
offset: { x: 10, y: 10 }
});
const toolsView = new joint.dia.ToolsView({
name: 'basic-tools',
tools: [removeButton]
});
view.addTools(toolsView);
joint.shapes.devs.ModelView = joint.dia.ElementView.extend(_.extend({},joint.shapes.basic.PortsViewInterface,{
initialize:function(){
joint.dia.ElementView.prototype.initialize.apply(this,arguments);
},
render:function(){
joint.dia.ElementView.prototype.render.apply(this,arguments);
this.renderTools();
this.update();
return this;
},
renderTools:function(){
var toolMarkup = this.model.toolMarkup || this.model.get('toolMarkup');
if (toolMarkup) {
var nodes = V(toolMarkup);
V(this.el).append(nodes);
}
return this;
},
pointerclick: function (evt, x, y) {
var className = evt.target.parentNode.getAttribute('class');
switch (className) {
case 'element-tool-remove':
this.model.remove();
return;
break;
default:
}
joint.dia.CellView.prototype.pointerclick.apply(this, arguments);
}
}));

How to add a jquery ui slider to each cell of slickgrid?

I need to add a jquery ui slider to each cell of slickgrid. Number of records is over 10,000 with over 150 columns. The problem is that the initial set of sliders are rendered fine but as I scroll (left or right), they disappear. Somehow, the grid is not getting invalidated for those cells. I am using the following formatter on the column:
SliderFormatter = function (row, cell, value, colDef, dataContext) {
var html = "<div class='mySlider' id='slider_" + dataContext['id'] + "'></div>" + value;
return html;
}
and invoking the slider from my document.ready callback.
Any help will be appreciated. Thanks in advance !
SlickGrid renders only what's visible in the viewport, plus a small buffer. As you scroll, rows/cells are dynamically added and removed.
What this means for your case, is that when you initialize the slider widget in your document.ready callback, you're only initializing a tiny portion of them, and the ones that did get initialized, don't get re-initialized when the cells they are in are removed and recreated by SlickGrid.
SlickGrid doesn't allow you to use jQuery widgets like the slider in cells and requires that formatters output pure HTML in order to make it hard to implement the grid in a way that will slow it down. (I won't get into my reasoning behind that admittedly controversial decision.)
My advice here is to avoid using the jQuery UI slider here. It is simply not scalable or performant enough. Without virtualization, what you're trying to do is impossible - initializing 100 sliders is going to be really slow; initializing 10'000 x 150 of them is out of the question. With virtualization, you'll need to initialize and destroy them on the fly as you're scrolling around, and that's just too slow to scroll smoothly.
Consider using an HTML5 range input - <INPUT type="range">. It's supported by all major browsers with the exception of IE <10. See http://caniuse.com/#feat=input-range.
I've created an example using SlickGrid's async post-render option. #Tin is probably right that it would be better to use the native <input type="range"> but just in case you need to support older browsers here's how you can do it.
function waitingFormatter(value) {
return '<div class="slider"></div>';
}
function renderSlider(cellNode, row, dataContext, colDef) {
var cell = $(cellNode);
cell.find('.slider').slider({
value: dataContext.value,
slide: function(e, ui) {
data[row].value = ui.value;
cell.prev().html(ui.value);
}
});
}
var grid;
var data = [];
var columns = [
{ id: "title", name: "Title", field: "title", sortable: false, width: 120, cssClass: "cell-title" },
{ id: "value", name: "Value", field: "value", sortable: false, editor: Slick.Editors.Integer, width: 40, validator: requiredFieldValidator },
{ id: "chart", name: "Slider", sortable: false, width: 425, formatter: waitingFormatter, rerenderOnResize: true, asyncPostRender: renderSlider }
];
var options = {
editable: true,
enableAddRow: false,
enableCellNavigation: true,
asyncEditorLoading: false,
enableAsyncPostRender: true
};
$(function () {
for (var i = 0; i < 500; i++) {
var d = (data[i] = {});
d["title"] = "Record " + i;
d["value"] = Math.round(Math.random() * 100);
}
grid = new Slick.Grid("#myGrid", data, columns, options);
})

Wijmo BarGraph - using an image on the axis

I am using Wijmo barcharts and am trying to create a graph which has images instead of labels on the x axis.
This is the code I have currently got, however, the image source is being printed out as a string rather than showing the image. Does anybody know of a way around this?
$(document).ready(function () {
defaultPalette = ['#e11a00', '#ddcd0e', '#005698'];
$("#wijbarchart").wijbarchart({
horizontal:false,
hint: {
content: function () {
return this.data.label + '\n ' + this.y + '';
}
},
seriesList: [{
label: "Entries",
legendEntry: true,
data: {
x: [<img src="photos/image1.jpg" />,<img src="photos/image2.jpg" />,<img src="photos/image3.jpg" />],
y: [22,10,65]
}
}],
painted: function (args) {
var bars = $(this).data('fields').chartElements.bars
if (bars.length > 0) {
for (var i in bars) {
bars[i].attr({ fill: defaultPalette[i % defaultPalette.length]});
}
}
},
mouseOut: function (e, data) {
data.bar.attr({ fill: defaultPalette[data.index % defaultPalette.length]});
}
});
});
Thanks!
Currently, there is no support for displaying images in place of valuelabels. However, I have made an enhancement for same to the development team and hopefully, it will be supported in future builds.
Regards
Ashish

Categories