FabricJS selection handling multiple objects - javascript

I am struggling with handling the selection of multiple objects. The desired behaviour would be that each object that is clicked will be added to the current selection. Similar to holding shift-key, but also selections using the drag-options should be added to the existing selection. The current behaviour of fabricjs is creating a new selection even when pressing shift-key. In addition the selection should not be cleared when clicking a blank space on the canvas. Deselecting objects should only be possible when clicking a single object which is part of the selection (when dragging selected objects should stay selected). Or by clicking an additional button to clear the full selection (with additional user confirmation).
I tried different setups using "selection:created" and "selection:updated" but this either messed up the selection or resulted in an endless loop because modifying the selection inside the update also triggers the update again.
canvas.on("selection:updated", (event) => {
event.selected.forEach((fabImg) => {
if (!this.selectedImages.includes(fabImg)) {
this.selectedImages.push(fabImg);
}
});
var groupSelection = new fabric.ActiveSelection(this.selectedImages);
canvas.setActiveObject(groupSelection);
});
Preventing the clear when clicking on the blank canvas was solved by:
var selection = [];
canvas.on("before:selection:cleared", (selected) => {
selection = this.canvas.getActiveObjects();
});
canvas.on("selection:cleared", (event) => {
var groupSelection = new fabric.ActiveSelection(selection);
canvas.setActiveObject(groupSelection);
});

Just in case someone else is interested, I ended up changing 3 functions in the fabricjs code to achieve the desired behaviour:
canvas.class.js:
_shouldClearSelection: function (e, target) {
var activeObjects = this.getActiveObjects(),
activeObject = this._activeObject;
return (
(target &&
activeObject &&
activeObjects.length > 1 &&
activeObjects.indexOf(target) === -1 &&
activeObject !== target &&
!this._isSelectionKeyPressed(e)) ||
(target && !target.evented) ||
(target &&
!target.selectable &&
activeObject &&
activeObject !== target)
);
}
just removed the check if an object was clicked, to stop deselecting when clicking on blank space.
_isSelectionKeyPressed: function (e) {
var selectionKeyPressed = false;
if (this.selectionKey == "always") {
return true;
}
if (
Object.prototype.toString.call(this.selectionKey) === "[object Array]"
) {
selectionKeyPressed = !!this.selectionKey.find(function (key) {
return e[key] === true;
});
} else {
selectionKeyPressed = e[this.selectionKey];
}
return selectionKeyPressed;
}
just adding a "dummy" key called "always" to pretend always holding the shift-key. In canvas definition just add this key:
this.canvas = new fabric.Canvas("c", {
hoverCursor: "hand",
selection: true,
backgroundColor: "#F0F8FF",
selectionBorderColor: "blue",
defaultCursor: "hand",
selectionKey: "always",
});
And in canvas_grouping.mixin.js:
_groupSelectedObjects: function (e) {
var group = this._collectObjects(e),
aGroup;
var previousSelection = this._activeObject;
if (previousSelection) {
if (previousSelection.type === "activeSelection") {
var currentActiveObjects = previousSelection._objects.slice(0);
group.forEach((obj) => {
if (!previousSelection.contains(obj)) {
previousSelection.addWithUpdate(obj);
}
});
this._fireSelectionEvents(currentActiveObjects, e);
} else {
aGroup = new fabric.ActiveSelection(group.reverse(), {
canvas: this,
});
this.setActiveObject(aGroup, e);
var objects = this._activeObject._objects.slice(0);
this._activeObject.addWithUpdate(previousSelection);
this._fireSelectionEvents(objects, e);
}
} else {
// do not create group for 1 element only
if (group.length === 1 && !previousSelection) {
this.setActiveObject(group[0], e);
} else if (group.length > 1) {
aGroup = new fabric.ActiveSelection(group.reverse(), {
canvas: this,
});
this.setActiveObject(aGroup, e);
}
}
}
This will extend existing groups on drag-select instead of overwriting the existing selection.

Related

Click event listener working but not keydown?

Why does this work:
buttonGrid.addEventListener('click', e => {
calculate(e);
});
let calculate = (e) => {
if (e.target.id === "back") {
displayArea.textContent = displayArea.textContent.substring(0,
displayArea.textContent.length - 1);
}
}
But not this...
buttonGrid.addEventListener('keydown', e => {
calculate(e);
});
let calculate = (e) => {
if (e.keyCode === 8) {
displayArea.textContent = displayArea.textContent.substring(0,
displayArea.textContent.length - 1);
}
}
See lines 30-45 at: https://jsfiddle.net/s9ebLdvt/2/
If I log out the typeof e.keyCode I get number. Hence I use a number rather than a string.
I've also tried switching the === to == but get the same issue.
Weirdly if I place a console log in the if statement it fires it just doesn't compute the code?

Event Listener on images ignored within modal window

I'm stuck as to why I can't get an AddEventListener click event to work on a set of images that appear in a modal. I had them working before before the modal aspect was involve, but I'm not sure that the modal broke the image click event either.
Here is the function in question, which is called within a massive document.addEventListener("DOMContentLoaded", function (event) function:
var attachClick = function () {
Array.prototype.forEach.call(containers, function (n, i) {
n.addEventListener('click', function (e) {
// populate
cleanDrawer();
var mediaFilterSelected = document.querySelector('.media-tags .tag-container .selected');
var selectedFilters = "";
if (mediaFilterSelected != "" && mediaFilterSelected != null) {
selectedFilters = mediaFilterSelected.innerHTML;
}
var portfolioItemName = '';
var selectedID = this.getAttribute('data-portfolio-item-id');
var data = portfolioItems.filter(function (item) {
portfolioItemName = item.name;
return item.id === selectedID;
})[0];
clientNameContainer.innerHTML = data.name;
descriptionContainer.innerHTML = data.description;
var childItems = data.child_items;
//We will group the child items by media tag and target the unique instance from each group to get the right main banner
Array.prototype.groupBy = function (prop) {
return this.reduce(function (groups, item) {
var val = item[prop];
groups[val] = groups[val] || [];
groups[val].push(item);
return groups;
}, {});
}
var byTag = childItems.groupBy('media_tags');
if (childItems.length > 0) {
handleBannerItem(childItems[0]);
var byTagValues = Object.values(byTag);
byTagValues.forEach(function (tagValue) {
for (var t = 0; t < tagValue.length; t++) {
if (tagValue[t].media_tags == selectedFilters) {
handleBannerItem(tagValue[0]);
}
}
});
childItems.forEach(function (item, i) {
// console.log("childItems.forEach"); we get into here
var img = document.createElement('img'),
container = document.createElement('div'),
label = document.createElement('p');
container.appendChild(img);
var mediaTags = item.media_tags;
container.className = "thumb";
label.className = "childLabelInactive thumbLbl";
thumbsContainer.appendChild(container);
if (selectedFilters.length > 0 && mediaTags.length > 0) {
for (var x = 0; x < mediaTags.length; x++) {
if (mediaTags[x] == selectedFilters) {
container.className = "thumb active";
label.className = "childLabel thumbLbl";
}
}
}
else {
container.className = i == 0 ? "thumb active" : "thumb";
// console.log("no tags selected"); we get to here
}
img.src = item.thumb;
if (item.media_tags != 0 && item.media_tags != null) {
childMediaTags = item.media_tags;
childMediaTags.forEach(function (cMTag) {
varLabelTxt = document.createTextNode(cMTag);
container.appendChild(label);
label.appendChild(varLabelTxt);
});
}
//console.log("before adding click to images"); we get here
console.log(img.src);
img.addEventListener("click", function () {
console.log("thumbnail clicked"); //this is never reached
resetThumbs();
handleBannerItem(item);
container.className = "thumb active";
});
});
}
attachClick();
//open a modal to show off the portfolio pieces for the selected client
var tingleModal = document.querySelector('.tingle-modal');
drawer.className = 'drawer';
var portfolioModal = new tingle.modal({
onOpen: function() {
if(tingleModal){
tingleModal.remove();
}
console.log('modal open');
},
onClose: function() {
console.log('modal closed');
//tingleModal.remove();
}
});
e.preventDefault();
portfolioModal.open();
portfolioModal.setContent(document.querySelector('.drawer-content').innerHTML);
});
});
};
And the specific bit that I'm having trouble with:
console.log(img.src);
img.addEventListener("click", function () {
console.log("thumbnail clicked"); //this is never reached
resetThumbs();
handleBannerItem(item);
container.className = "thumb active";
});
I tried removing the e.PreventDefault() bit but that didn't solve the issue. I know the images are being created, so the img variable isn't empty. I feel like the addEventListener is setup correctly. I also tried moving that bit up just under the img.src = item.thumb line, but no luck. For Some reason, the click event just will not trigger for the images.
So if I understand correctly, you have a modal that lies above the images (it has a higher z-index)? Well in this case the clicks are not reaching the images as they will hit the modal. You can pass clicks through elements that lie above by applying the css property pointer-events: none; to the modal, but thats somehow controversial to what a modal is intended to do.
Are the images present in the modal on DOMContentLoaded? You may be able to try delegating the handling of clicks to a parent element if that's the case.
You can try the delegation approach shown here: Vanilla JavaScript Event Delegation

I want to make left click behave as right click using either JS or jQuery

I have been trying to trigger right click even when the users left click.
I have tried trigger, triggerHandler, mousedown but I wasn't able to get it to work.
I'm able to catch the click events themselves but not able to trigger the context menu.
Any ideas?
To trigger the mouse right click
function triggerRightClick(){
var evt = new MouseEvent("mousedown", {
view: window,
bubbles: true,
cancelable: true,
clientX: 20,
button: 2
});
some_div.dispatchEvent(evt);
}
To trigger the context menu
function triggerContextMenu(){
var evt = new MouseEvent("contextmenu", {
view: window
});
some_div.dispatchEvent(evt);
}
Here is the bin: http://jsbin.com/rimejisaxi
For better reference/explanation: https://stackoverflow.com/a/7914742/1957036
Use the following code for reversing mouse clicks.
$.extend($.ui.draggable.prototype, {
_mouseInit: function () {
var that = this;
if (!this.options.mouseButton) {
this.options.mouseButton = 1;
}
$.ui.mouse.prototype._mouseInit.apply(this, arguments);
if (this.options.mouseButton === 3) {
this.element.bind("contextmenu." + this.widgetName, function (event) {
if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) {
$.removeData(event.target, that.widgetName + ".preventClickEvent");
event.stopImmediatePropagation();
return false;
}
event.preventDefault();
return false;
});
}
this.started = false;
},
_mouseDown: function (event) {
// we may have missed mouseup (out of window)
(this._mouseStarted && this._mouseUp(event));
this._mouseDownEvent = event;
var that = this,
btnIsLeft = (event.which === this.options.mouseButton),
// event.target.nodeName works around a bug in IE 8 with
// disabled inputs (#7620)
elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false);
if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) {
return true;
}
this.mouseDelayMet = !this.options.delay;
if (!this.mouseDelayMet) {
this._mouseDelayTimer = setTimeout(function () {
that.mouseDelayMet = true;
}, this.options.delay);
}
if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
this._mouseStarted = (this._mouseStart(event) !== false);
if (!this._mouseStarted) {
event.preventDefault();
return true;
}
}
// Click event may never have fired (Gecko & Opera)
if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) {
$.removeData(event.target, this.widgetName + ".preventClickEvent");
}
// these delegates are required to keep context
this._mouseMoveDelegate = function (event) {
return that._mouseMove(event);
};
this._mouseUpDelegate = function (event) {
return that._mouseUp(event);
};
$(document)
.bind("mousemove." + this.widgetName, this._mouseMoveDelegate)
.bind("mouseup." + this.widgetName, this._mouseUpDelegate);
event.preventDefault();
mouseHandled = true;
return true;
}
});
Now at the function calling event use mouseButton : 3 for right click and 1 for left click

how to arcgis javascript vertex undoManager

I try to undo / redo the edit vertex using undomanager.
Graphic objects are tested. But I do not know what to do Edit vertex undo / redo.
Is it possible the vertex undo / redo?
I looked to find many examples have not found the answer.
i`m korean beginner programmer. help me~ T.T
function initEditing(evt) {
console.log("initEditing", evt);
var currentLayer = null;
var layers = arrayUtils.map(evt.layers, function(result) {
return result.layer;
console.log("result ==== "+result);
});
console.log("layers", layers);
editToolbar = new Edit(map);
editToolbar.on("deactivate", function(evt) {
console.log("deactivate !!!! ");
currentLayer.applyEdits(null, [evt.graphic], null);
});
arrayUtils.forEach(layers, function(layer) {
var editingEnabled = false;
layer.on("dbl-click", function(evt) {
event.stop(evt);
if (editingEnabled === false) {
editingEnabled = true;
editToolbar.activate(Edit.EDIT_VERTICES , evt.graphic);
pre_evt = evt.graphic;
editToolbar.on("vertex-move-stop", function(evt){
console.log("vertex-move-stop~");
g_evt = evt;
console.log("evt.transform ===== " +evt.transform);
var operation = new esri.dijit.editing.Update({
featureLayer : landusePointLayer,
preUpdatedGraphics:pre_evt,
postUpdatedGraphics: evt.graphic
})
var operation = new CustomOperation.Add({
graphicsLayer: pre_evt._graphicsLayer,
addedGraphic: evt.graphic
});
undoManager.add(operation);
console.log("operation ======== ",operation);
});
console.log("dbl-click & eidt true");
} else {
currentLayer = this;
editToolbar.deactivate();
editingEnabled = false;
console.log("dbl-click & eidt false ");
}
});
The sample you are refering to, just gives you an idea how you can use UndoManager. You need to create your own operations if you need undo/redo for vertices. Below I have provided one for AddVertex. You would need to create your own for other operations.
define(["dojo/_base/declare",
"esri/OperationBase"],
function(declare,
OperationBase) {
var customOp = {};
customOp.AddVertex = declare(OperationBase, {
label: "Add Vertex",
_editedGraphic: null,
_vertexInfo: null,
_vertex: null,
_editTool: null,
constructor: function (params) {
params = params || {};
if (!params.editTool) {
console.error("no edit toolbar provided");
return;
}
this._editTool = params.editTool;
if (!params.editedGraphic) {
console.error("no graphics provided");
return;
}
this._editedGraphic = params.editedGraphic;
if (!params.vertexinfo) {
console.error("no vertexinfo provided");
return;
}
this._vertexInfo = params.vertexinfo;
var geometry = this._editedGraphic.geometry;
if(geometry.type === "multipoint") {
this._vertex = geometry.getPoint(this._vertexInfo.pointIndex);
} else if(geometry.type === "polyline" || geometry.type === "polygon") {
this._vertex = geometry.getPoint(this._vertexInfo.segmentIndex, this._vertexInfo.pointIndex);
} else {
console.error("Not valid geometry type.");
}
},
performUndo: function () {
var geometry = this._editedGraphic.geometry;
if(geometry.type === "multipoint"){
geometry.removePoint(this._vertexInfo.pointIndex);
} else if(geometry.type === "polyline" || geometry.type === "polygon") {
geometry.removePoint(this._vertexInfo.segmentIndex, this._vertexInfo.pointIndex);
}
this._editedGraphic.draw();
this._editTool.refresh();
},
performRedo: function () {
var geometry = this._editedGraphic.geometry;
if(geometry.type === "multipoint"){
geometry.removePoint(this._vertexInfo.pointIndex, this._vertex);
} else if(geometry.type === "polyline" || geometry.type === "polygon") {
geometry.insertPoint(this._vertexInfo.segmentIndex, this._vertexInfo.pointIndex, this._vertex);
}
this._editedGraphic.draw();
this._editTool.refresh();
}
});
return customOp;
});
Make sure you clear the UndoManager when you deactivate the edit toolbar. Otherwise the Operations will remain. Do not combine the Add graphics operations with Vertex Operations. It will not work as they use different toolbar and edit toolbar state will be lost as soon as you deactive it.
One more thing to note is when you use the UndoManager, the graphics isModified state will always be true, since we are adding and deleting vertex during undo/redo, even if undo all changes. Hence, make sure you need to applyedit by checking if there are any pending undo (geometry is really modified).
Hope this was helpful.

DOM object only requires two clicks in Internet Explorer

I'm really struggling with the following bit of code. I'm still really new to using DOM with Javascript and this script is running flawlessly in FireFox, Chrome and Safari. In Internet Explorer it requires two clicks. If you visit the link in FireFox and then the same link in Internet Explorer you'll see that if you click a shape in FireFox it immediately shows the colour options if you do this in Internet Explorer it will not show the colour options until you've clicked on the shape twice or on a shape and then another shape. Can an IE, DOM, Javascript Ninja tell me what's wrong with the script that cause the need for two clicks in IE?
<?php
$swatches = $this->get_option_swatches();
?>
<script type="text/javascript">
document.observe('dom:loaded', function() {
try {
var swatches = <?php echo Mage::helper('core')->jsonEncode($swatches); ?>;
function find_swatch(key, value) {
for (var i in swatches) {
if (swatches[i].key == key && swatches[i].value == value)
return swatches[i];
}
return null;
}
function has_swatch_key(key) {
for (var i in swatches) {
if (swatches[i].key == key)
return true;
}
return false;
}
function create_swatches(label, select) {
// create swatches div, and append below the <select>
var sw = new Element('div', {'class': 'swatches-container'});
select.up().appendChild(sw);
// store these element to use later for recreate swatches
select.swatchLabel = label;
select.swatchElement = sw;
// hide select
select.setStyle({position: 'absolute', top: '-9999px'});
$A(select.options).each(function(opt, i) {
if (opt.getAttribute('value')) {
var elm;
var key = trim(opt.innerHTML);
// remove price
if (opt.getAttribute('price')) key = trim(key.replace(/\+([^+]+)$/, ''));
var item = find_swatch(label, key);
if (item)
elm = new Element('img', {
src: '<?php echo Mage::getBaseUrl(Mage_Core_Model_Store::URL_TYPE_MEDIA); ?>swatches/'+item.img,
alt: opt.innerHTML,
title: opt.innerHTML,
'class': 'swatch-img'});
else {
console.debug(label, key, swatches);
elm = new Element('a', {'class': 'swatch-span'});
elm.update(opt.innerHTML);
}
elm.observe('click', function(event) {
select.selectedIndex = i;
fireEvent(select, 'change');
var cur = sw.down('.current');
if (cur) cur.removeClassName('current');
elm.addClassName('current');
});
sw.appendChild(elm);
}
});
}
// Hide Second Option's Label
function hideStuff(id) {
if (document.getElementById(id)) {
document.getElementById(id).style.display = 'none';
}
}
hideStuff("last-option-label");
function showStuff(id) {
if (document.getElementById(id)) {
document.getElementById(id).style.display = '';
}
}
function recreate_swatches_recursive(select) {
// remove the old swatches
if (select.swatchElement) {
select.up().removeChild(select.swatchElement);
select.swatchElement = null;
}
// create again
if (!select.disabled)
showStuff("last-option-label");
create_swatches(select.swatchLabel, select);
// recursively recreate swatches for the next select
if (select.nextSetting)
recreate_swatches_recursive(select.nextSetting);
}
function fireEvent(element,event){
if (document.createEventObject){
// dispatch for IE
var evt = document.createEventObject();
return element.fireEvent('on'+event, evt);
}
else{
// dispatch for firefox + others
var evt = document.createEvent("HTMLEvents");
evt.initEvent(event, true, true ); // event type,bubbling,cancelable
return !element.dispatchEvent(evt);
}
}
function trim(str) {
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
$$('#product-options-wrapper dt').each(function(dt) {
// get custom option's label
var label = '';
$A(dt.down('label').childNodes).each(function(node) {
if (node.nodeType == 3) label += node.nodeValue;
});
label = trim(label);
var dd = dt.next();
var select = dd.down('select');
if (select && has_swatch_key(label)) {
create_swatches(label, select);
// if configurable products, recreate swatches of the next select when the current select change
if (select.hasClassName('super-attribute-select')) {
select.observe('change', function() {
recreate_swatches_recursive(select.nextSetting);
});
}
}
});
}
catch(e) {
alert("Color Swatches javascript error. Please report this error to support#galathemes.com. Error:" + e.message);
}
});
</script>
console.debug has been deprecated. In the "function create_swatches(label, select)" where it is written "console.debug(label, key, swatches);" change it to "console.log(label, key, swatches);" or you can just delete that line of code all together....... thaterrorbegone

Categories