Cologne_Muc
Cologne_Muc

Reputation: 753

Fabric.js - Grouped iText not editable

The title says all. When a fabric.ITextis part of a fabric.Group, it no longer reacts like a editable text.

Here is a use-case: http://jsfiddle.net/vAjYd/

Is there a way to solve this problem? Otherwise I have to write my on group.

Btw: A fabric.Group has many advantages, but the disabled eventing makes it impossible to use for UI-Elements (i.e. this use-case or groups of buttons as groups of texts and rects).

Upvotes: 14

Views: 9830

Answers (6)

AJLoveChina
AJLoveChina

Reputation: 614

I am the last answer, but the better answer, A gif demo is worth a thousand words enter image description here

as you can see, it works great, link address https://jsfiddle.net/hejie/rhf069ob/latest

The solution:

  1. create a group
  2. double click on the group, hide the textbox inside group
  3. create a temporary textbox for editing, the same position as the textbox inside group
  4. after editing on temporary textbox, commit new text to textbox inside group, and remove the temporary textbox
  5. done.

Why is this solution better than group/ungroup ?

  1. ungroup => editing => group, will create new group instance, the original group instance is destroyed, this is very harmful for your objects state management
  2. very difficult to edit after you rotate the group, because the textbox insdie group is also rotated.

In brief, do not use group/ungroup solution! Try following code :

var canvas = new fabric.Canvas('canvas');
canvas.setWidth(300);
canvas.setHeight(300);

function createGroup() {
    let circle = new fabric.Circle({
    radius:40,
    fill: 'rgba(200, 0, 0, 0.3)',
    originX: 'center',
    originY: 'center',
  });
  
  let text = new fabric.Textbox("AJLoveChina", {
    originX: 'center',
    originY: 'center',
    textAlign: 'center',
    fontSize: 12,
  })
  
  let group = new fabric.Group([circle, text], {
    left: 100,
    top: 100,
    originX: 'center',
    originY: 'center',
  });
  
  group.on('mousedblclick', () => {
    // textForEditing is temporary obj, 
    // and will be removed after editing
    let textForEditing = new fabric.Textbox(text.text, {
      originX: 'center',
      originY: 'center',
      textAlign: text.textAlign,
      fontSize: text.fontSize,
      
      left: group.left,
      top: group.top,
    })
    
    // hide group inside text
    text.visible = false;
    // note important, text cannot be hidden without this
    group.addWithUpdate();
    
    textForEditing.visible = true;
    // do not give controls, do not allow move/resize/rotation on this 
    textForEditing.hasConstrols = false;

    
    // now add this temporary obj to canvas
    canvas.add(textForEditing);
    canvas.setActiveObject(textForEditing);
    // make the cursor showing
    textForEditing.enterEditing();
    textForEditing.selectAll();
    
    
    // editing:exited means you click outside of the textForEditing
    textForEditing.on('editing:exited', () =>{
        let newVal = textForEditing.text;
      let oldVal = text.text;
      
      // then we check if text is changed
      if (newVal !== oldVal) {
        text.set({
            text: newVal,
          visible: true,
        })
        
        // comment before, you must call this
        group.addWithUpdate();
        
        // we do not need textForEditing anymore
        textForEditing.visible = false;
        canvas.remove(textForEditing);
        
        // optional, buf for better user experience
        canvas.setActiveObject(group);
      }
    })
  })
  
  canvas.add(group);
}

createGroup();
#canvas {
  width:300px;
  height:300px;
  border: 1px solid #333;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.4.0/fabric.min.js"></script>
<h4>fabricjs editing textbox inside fabric Group</h4>
<canvas id="canvas"></canvas>

Upvotes: 2

jsperafico
jsperafico

Reputation: 21

I know is too late, but here is a working code for your problem: http://jsfiddle.net/fd4yrxq6/2/

I just made some little changes using the anwares above:

...
// ungroup objects in group
var items;
var ungroup = function (group) {
    items = group._objects;
    group._restoreObjectsState();
    canvas.remove(group);
    canvas.renderAll();
    for (var i = 0; i < items.length; i++) {
        canvas.add(items[i]);
    }
    // if you have disabled render on addition
    canvas.renderAll();
};
...
dimensionText.on('editing:exited', function () {
        for (var i = 0; i < items.length; i++) {
        canvas.remove(items[i]);
    }
    var grp = new fabric.Group(items, {});
    canvas.add(grp);
    grp.on('mousedown', fabricDblClick(grp, function (obj) {
        ungroup(grp);
        canvas.setActiveObject(dimensionText);
        dimensionText.enterEditing();
        dimensionText.selectAll();
    }));
});
...

var canvas = new fabric.Canvas('canvas');
canvas.setWidth(300);
canvas.setHeight(300);

// Double-click event handler
var fabricDblClick = function (obj, handler) {
    return function () {
        if (obj.clicked) handler(obj);
        else {
            obj.clicked = true;
            setTimeout(function () {
                obj.clicked = false;
            }, 500);
        }
    };
};

// ungroup objects in group
var items;
var ungroup = function (group) {
    items = group._objects;
    group._restoreObjectsState();
    canvas.remove(group);
    canvas.renderAll();
    for (var i = 0; i < items.length; i++) {
        canvas.add(items[i]);
    }
    // if you have disabled render on addition
    canvas.renderAll();
};

// Re-group when text editing finishes
var dimensionText = new fabric.IText("Dimension Text", {
    fontFamily: 'Comic Sans',
    fontSize: 14,
    stroke: '#000',
    strokeWidth: 1,
    fill: "#000",
    left: 170,
    top: 60
});
dimensionText.on('editing:exited', function () {
        for (var i = 0; i < items.length; i++) {
        canvas.remove(items[i]);
    }
    var grp = new fabric.Group(items, {});
    canvas.add(grp);
    grp.on('mousedown', fabricDblClick(grp, function (obj) {
        ungroup(grp);
        canvas.setActiveObject(dimensionText);
        dimensionText.enterEditing();
        dimensionText.selectAll();
    }));
});

function addRuler() {
    var dimension_mark = new fabric.Path('M0,0L0,-5L0,5L0,0L150,0L150,-5L150,5L150,0z');
    dimension_mark.set({
        left: 150,
        top: 70,
        stroke: '#333333',
        strokeWidth: 2,
        scaleY: 1
    });
    var dimension_group = new fabric.Group([dimension_mark, dimensionText], {
        left: 50,
        top: 50
    });
    canvas.add(dimension_group);
    dimension_group.on('mousedown', fabricDblClick(dimension_group, function (obj) {
        ungroup(dimension_group);
        canvas.setActiveObject(dimensionText);
        dimensionText.enterEditing();
        dimensionText.selectAll();
        canvas.renderAll();
    }));
}
addRuler();

style = {
        left: 100,
        top: 100,
        width: 20,
        height: 20,
        fill: "green",
        lockRotation: true
    };
    var rectangle = new fabric.Polygon([
        { x: 20, y: 0 },
        { x: 0, y: 20 },
        { x: 20, y: 40 },
        { x: 40, y: 20 },
    ], style);
    canvas.add(rectangle);
.canvas {
    border: 1px solid Black;
    width: 300px;
    height: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.2.0/fabric.min.js"></script>
<p id="addObj">Fabric.js - iText editing from Group</p>
<canvas id="canvas" class="canvas" ></canvas>

Upvotes: 2

Lin Weishuo
Lin Weishuo

Reputation: 1

If you would like a real visual padding(like the css) for itext, you could add an unselectable Rect just behind the itext. And adjust the Rect position based on all event for itext, e.g, moving, scaling etc.

self.createITextWithPadding = function (event) {

               var itext = new fabric.IText('Done', {
                   fontSize: 18,
                   padding : tnbConstants.ITEXT.PADDING,
                   fill: '#FFF',

               });

               itext.left = (event.pageX - $("#tnb-panel").position().left)-itext.width/2;
               itext.top = (event.pageY-$("#tnb-panel").position().top)-itext.height/2;

               var bg = new fabric.Rect({
                    fill: '#32b775',
                   left : itext.left - tnbConstants.ITEXT.PADDING,
                   top :itext.top - tnbConstants.ITEXT.PADDING,
                   rx: 5,
                   ry: 5,
                   width: itext.width + tnbConstants.ITEXT.PADDING*2,
                   height:itext.height + tnbConstants.ITEXT.PADDING*2,
                   selectable : false
               });

               itext.bgRect = bg;
               itext.on("moving",self.adjustBackRect);
               itext.on("scaling",self.adjustBackRect);

               return itext;
           }

self.adjustBackRect = function (e) {//e is event
               var text = e.target;
               var bgRect = text.bgRect;

               bgRect.set({
                   left : text.left - text.padding,
                   top : text.top - text.padding,
                   width : text.width * text.scaleX + text.padding * 2,
                   height : text.height * text.scaleY + text.padding * 2 ,
               });


               console.log("text width :" + (text.width * text.scaleX + text.padding*2));
               console.log("text height :" + (text.height * text.scaleY + text.padding*2));
               console.log("bg width :" + (bgRect.width ));
               console.log("bg height :" + (bgRect.height ));
           };

Upvotes: 0

user6821300
user6821300

Reputation: 111

Thanks for the answer, However it doesn't make sure only the items in the group are regrouped, we must change 'canvas.forEachObject' to 'groupItems' as it will try to put back all items in the canvas only those belonged to the group. See the following code

// ungroup objects in group
var groupItems = []
var ungroup = function (group) {
    groupItems = group._objects;
    group._restoreObjectsState();
    canvas.remove(group);
    for (var i = 0; i < groupItems.length; i++) {
        canvas.add(groupItems[i]);
    }
    // if you have disabled render on addition
    canvas.renderAll();
};

// Re-group when text editing finishes
var dimensionText = new fabric.IText("Dimension Text", {
    fontFamily: 'Comic Sans',
    fontSize: 14,
    stroke: '#000',
    strokeWidth: 1,
    fill: "#000",
    left: 170,
    top: 60
});
dimensionText.on('editing:exited', function () {
    var items = [];
    groupItems.forEach(function (obj) {
        items.push(obj);
        canvas.remove(obj);
    });
    var grp = new fabric.Group(items.reverse(), {});
    canvas.add(grp);
    grp.on('mousedown', fabricDblClick(grp, function (obj) {
        ungroup(grp);
        canvas.setActiveObject(dimensionText);
        dimensionText.enterEditing();
        dimensionText.selectAll();
    }));
});

Upvotes: 1

saurabh
saurabh

Reputation: 757

I might be too late to answer this, but surely many of you like me searching for this answer shall meet the solution. You can find a perfectly working solution of iText where user can edit the text of iText which is a part of a fabric.group Working solution

Approach followed here is:

  1. When user double clicks the desired group, we ungroup all the elements
  2. set focus on iText element
  3. Enters the editing mode of iText & select all text so that user can directly start editing..
  4. Once user exits edit mode, the elements are again grouped together.

    // ungroup objects in group
    var items
    var ungroup = function (group) {
        items = group._objects;
        group._restoreObjectsState();
        canvas.remove(group);
        for (var i = 0; i < items.length; i++) {
            canvas.add(items[i]);
        }
        // if you have disabled render on addition
        canvas.renderAll();
    };
    
    // Re-group when text editing finishes
    var dimensionText = new fabric.IText("Dimension Text", {
        fontFamily: 'Comic Sans',
        fontSize: 14,
        stroke: '#000',
        strokeWidth: 1,
        fill: "#000",
        left: 170,
        top: 60
    });
    dimensionText.on('editing:exited', function () {
        var items = [];
        canvas.forEachObject(function (obj) {
            items.push(obj);
            canvas.remove(obj);
        });
        var grp = new fabric.Group(items.reverse(), {});
        canvas.add(grp);
        grp.on('mousedown', fabricDblClick(grp, function (obj) {
            ungroup(grp);
            canvas.setActiveObject(dimensionText);
            dimensionText.enterEditing();
            dimensionText.selectAll();
        }));
    });
    

Upvotes: 26

Alexander Kludt
Alexander Kludt

Reputation: 863

Currently there is no way for IText to handle the events as they are not handed down to it if it's contained in a group. I think this is also the prefered way to handle that as it would confuse the user - what if he starts to edit multiple texts. This might end up in a mess. Maybe you can rework your script a little to workaround this problems.

Upvotes: 0

Related Questions