Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <script src="https://unpkg.com/konva@8/konva.min.js"></script>
<p>A class to provide a border around a group, and to enable click-drag on group white-space. <br />
 Drag shapes and group background, click group to transform, click stage to end transform.  <br />
 Red border should follow group but is not part of it! <br />
  Yellow box acts as indicator that zIndex is working correctly.
</p>
<p><button id='toggleBorder'>Toggle border rect</button></p>

<div id="container"></div> 
              
            
!

CSS

              
                body {
  margin: 20px;
  padding: 0;
  overflow: hidden;
  background-color: #f0f0f0;
}
 
              
            
!

JS

              
                // control data for the border
const settings = {
  padding: 20,
  fill: 'transparent',
  opacity: 0.9,
  stroke: 'red',
  strokeWidth: 1,
  visible: true,
  bgRectFill: 'transparent',  // <- change to 'lime' to see the background rect.
}
 
/*
* This is the GroupBorder class which draws the border and makes the group draggable.
* The constructor expects a Konva.Group, a plain JS object containing settings as per the example above, and the Konva.Transformer.
*/

class GroupBorder {
  
  group = null;
//  groupRect = new Konva.Rect({name: 'groupBackgroundRect'});
  borderRect = new Konva.Rect({name: 'groupBorderRect'});
  padding = 0;
  showBorder = true;
  transformer = null;
  dragStartPos = {};
  dragStartGroupPos = {};
  transforming=false;

  constructor(group, settings, transformer){
    
    this.group = group;
    this.padding = settings.padding; // get the padding from the settings object.
    this.showBorder = settings.visible;
    this.transformer = transformer;
    
    /* set up the border rectangle. A few lines related to setting fill opacity independent of stroke */
    const opacity = (settings.fill === 'transparent') ? 0 : settings.opacity;
    const colorParts = Konva.Util.getRGB(settings.fill); // + (255 * settings.opacity).toString(16);
    function getHex(val){
      return (val.toString(16).length < 2)?'0'+ val.toString(16) : val.toString(16);
    }
    const fillColor = '#' + getHex(colorParts.r) + getHex(colorParts.g) +  getHex(colorParts.b) + getHex(Math.floor(255 * opacity));

    // Now that's over, create the rect
    this.borderRect.setAttrs({stroke: settings.stroke, strokeWidth: settings.strokeWidth, fill: fillColor, visible: settings.visible, draggable: true});

    // In some cases, 'this' has a special meaning such as within event listeners, so we set a variable to point to this class.
    const that = this;
    
    // The border rect can be dragged giving the ability to drag the group. Note the rect and group positions at drag start
    this.borderRect.on('dragstart', function(){
      const shape = this; 
      that.dragStartPos = shape.position() 
      that.dragStartGroupPos = group.position() 
    })    
    
    // On each fire of the dragmove event, compute the distance moved since start and apply to the group.
    this.borderRect.on('dragmove', function(){      
      const shape = this,
            pos = shape.position(),
            daltaPos = {x: pos.x - that.dragStartPos.x, y: pos.y - that.dragStartPos.y};
      
      group.position({x: that.dragStartGroupPos.x + daltaPos.x, y: that.dragStartGroupPos.y + daltaPos.y });
    })
    
    
    // If the user clicks on the border rect then we enable the transformer
    this.borderRect.on('click', function(evt){
      that.transformer.nodes([]); // clear transformer nodes
      that.startTransform(); // call before setting transformer nodes!
      that.transformer.nodes([group]); // set transformer nodes
      evt.cancelBubble = true;   // stop the event passing up the parents
    })    
    
    // Add the border rect to the same parent as the group
    this.group.getParent().add(this.borderRect); 

    // Important - must ensure that border rect is one step below group it borders in the zIndex list.
    this.borderRect.setAttrs({zIndex: this.group.zIndex()}); 
    
    this.moveBorder(); // invoke the moveBorder function to draw the border the first time.
  }
  
  // Show & hide the border rect.
  toggleBorder(){
    this.showBorder = !this.showBorder;
    this.borderRect.visible(this.showBorder);
  }
  
  // Set the transforming flag and the transformer padding to match the border rect padding
  startTransform(){ 
    this.transformer.padding(this.padding);
  }
  
  endTransform(){
    // nothing to do here !
  }
  
 
  /* Function to do all the manipulation of the border and background rects*/
  moveBorder(){
 
    const r = this.group.getClientRect({skipTransform: true}), // we need the bounding rect of the group without scale or rotation.
          
          // the pt is the top-left of the bounding rect without scaling or rotation, plus anti-scaled padding width. This width is anti-scaled
          // because in the next step any scaling in use on the group will be applied to that point and the padding would be scaled, so we 
          // anti-scale it first so that the final border size is correct even when the group is scaled.
          pt = {x: r.x - this.padding / this.group.scaleX(), y: r.y - this.padding / this.group.scaleY()},
          
          // The group.getAbsoluteTransform().point() function takes any given point and returns that point with the group transform applied.
          pos = group.getAbsoluteTransform().point(pt);

    // Set the position and size of the border rect using position calculated above, with size based on untransformed bounding rect 
    // with scale applied, and with same rotation as the group.
    this.borderRect.setAttrs({
                         x: pos.x, 
                         y: pos.y,
                         width: (r.width * this.group.scaleX()) + (2 * this.padding),
                         height: (r.height * this.group.scaleY()) + (2 * this.padding),
                         rotation: this.group.rotation(),
                        });
  }  
}
// End of GroupBorder class

/*
* From here onwards we set up the stage and its contents.
*/
const stage = new Konva.Stage({
        container: 'container',
        width: window.innerWidth,
        height: window.innerHeight 
      }),
      layer = new Konva.Layer(),
      group = new Konva.Group({draggable: true}),
      circle = new Konva.Circle({ x: 300, y: 100, radius: 50, fill: 'cyan', draggable: 'true'}),
      star = new Konva.Star({ x: 350, y: 100, outerRadius: 50, fill: 'magenta', numPoints: 6, innerRadius: 20, draggable: 'true'}),
      rect = new Konva.Rect({ x: 100, y: 100, width: 200, height: 200, fill: 'gold', draggable: 'true'}),
      transformer = new Konva.Transformer();
      
stage.add(layer);
group.add(circle, star);
layer.add(rect, group);  // the yellow rect is added to confirm zIndex changes work as expected. 
layer.add(transformer); // Add the transformer to the latyer

// Create the group border rect.
const borderThang = new GroupBorder(group, settings, transformer);
 
/*
** Any shape in the group must call the moveBorder fn in its dragmove listener. 
*/
circle.on('dragmove', function(){
  borderThang.moveBorder()
})
 
star.on('dragmove', function(){
  borderThang.moveBorder()
})

// The group must call the moveBorder fn in any drag listener
group.on('dragstart', function(){
  borderThang.moveBorder()
})
group.on('dragmove', function(){
  borderThang.moveBorder()
})
group.on('dragend', function(){
  borderThang.moveBorder()
})

// The group must call startTransform() when a transformer is enabled.
group.on('click', function(evt){
  clearTransformer()
  borderThang.startTransform();  
  transformer.nodes([group]);
  evt.cancelBubble = true;   
})

// becuase grouped shapes can be dragged we might need to close the transformer if is is open
group.on('mousedown', function(evt){
  clearTransformer();
  evt.cancelBubble = true;  
})

// Update the border rect position via the moveBorder function.
group.on('transform', function(){
  borderThang.moveBorder();
})

// We click the stage empty space to hide the transformer
stage.on('click', function(){
 clearTransformer();
})

// Util fundtion to clear the transformer and tell the border rect that transforming has stopped/
function clearTransformer(){
   transformer.nodes([]);
   borderThang.endTransform();
}

// Button to show/hide the border rect.
$('#toggleBorder').on('click', function(){
  borderThang.toggleBorder();
})
              
            
!
999px

Console