For example, we have an IText "text1" with underline from 2nd till 4th char.
We need to copy its complete style to "text2" like this:
So here we must insert some function like getStyleDeclaration or getSelectionStyles for "text1" and setSelectionStyles for "text2", but I still can't make it work.
var text1 = new fabric.IText('text1', {
left: 100,
top: 100,
});
text1.setSelectionStyles({ underline: true, }, 2, 4);
var text2 = new fabric.IText('text2', {
left: 200,
top: 200,
});
function createCanvas(id) {
canvas = new fabric.Canvas(id);
canvas.add(text1);
canvas.add(text2);
return canvas;
}
You can just use styles property in text object
const styles = text1.styles;
top-level properties -> line numbers, 2nd-level properties - charater numbers
In your case you'll get
{
"0": { // line number
"2":{ // charater number
"underline":true
},
"3":{ // charater number
"underline":true
}
}
}
http://fabricjs.com/docs/fabric.Text.html#styles
Related
I'm trying to create a growing TextEditor as input for a chat view.
The goal is to have a box which expands until 6 lines are reached for example. After that it should be scrollable.
I already managed to do this with strings, which contain line breaks \n.
TextEditor(text: $composedMessage)
.onChange(of: self.composedMessage, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
height = startHeight + CGFloat((value.numberOfLines() * 20))
}
if value.numberOfLines() == 0 || value.isEmpty {
height = 50
}
})
})
I created a string extension which returns the number of line breaks by calling string.numberOfLines() var startHeight: CGFloat = 50
The problem: If I paste a text which contains a really long text, it's not expanding when this string has no line breaks. The text get's broken in the TextEditor.
How can I count the number of breaks the TextEditor makes and put a new line character at that position?
Here's a solution adapted from question and answer,
struct ChatView: View {
#State var text: String = ""
// initial height
#State var height: CGFloat = 30
var body: some View {
ZStack(alignment: .topLeading) {
Text("Placeholder")
.foregroundColor(.appLightGray)
.font(Font.custom(CustomFont.sofiaProMedium, size: 13.5))
.padding(.horizontal, 4)
.padding(.vertical, 9)
.opacity(text.isEmpty ? 1 : 0)
TextEditor(text: $text)
.foregroundColor(.appBlack)
.font(Font.custom(CustomFont.sofiaProMedium, size: 14))
.frame(height: height)
.opacity(text.isEmpty ? 0.25 : 1)
.onChange(of: self.text, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
// new height
height = 120
}
if value.numberOfLines() == 0 || value.isEmpty {
// initial height
height = 30
}
})
})
}
.padding(4)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.appLightGray, lineWidth: 0.5)
)
}
}
And the extension,
extension String {
func numberOfLines() -> Int {
return self.numberOfOccurrencesOf(string: "\n") + 1
}
func numberOfOccurrencesOf(string: String) -> Int {
return self.components(separatedBy:string).count - 1
}
}
I found a solution!
For everyone else trying to solve this:
I added a Text with the same width of the input field and then used a GeometryReader to calculate the height of the Text which automatically wraps. Then if you divide the height by the font size you get the number of lines.
You can make the text field hidden (tested in iOS 14 and iOS 15 beta 3)
If you're looking for an iOS 15 solution, I spent a while and figured it out. I didn't want to have to resort to UIKit or ZStacks with overlays or duplicative content as a "hack". I wanted it to be pure SwiftUI.
I ended up creating a separate struct that I could reuse anywhere I needed it, as well as add additional parameters in my various views.
Here's the struct:
struct FieldMultiEntryTextDynamic: View {
var text: Binding<String>
var body: some View {
TextEditor(text: text)
.padding(.vertical, -8)
.padding(.horizontal, -4)
.frame(minHeight: 0, maxHeight: 300)
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.primary)
.dynamicTypeSize(.medium ... .xxLarge)
.fixedSize(horizontal: false, vertical: true)
} // End Var Body
} // End Struct
The cool thing about this is that you can have placeholder text via an if statement and it supports dynamic type sizes.
You can implement it as follows:
struct MyView: View {
#FocusState private var isFocused: Bool
#State private var myName: String = ""
var body: some View {
HStack(alignment: .top) {
Text("Name:")
ZStack(alignment: .trailing) {
if myName.isEmpty && !isFocused {
Text("Type Your Name")
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.secondary)
}
HStack {
VStack(alignment: .trailing, spacing: 5) {
FieldMultiEntryTextDynamic(text: $myName)
.multilineTextAlignment(.trailing)
.keyboardType(.alphabet)
.focused($isFocused)
}
}
}
}
.padding()
.background(.blue)
}
}
Hope it helps!
So adding an SVG to the canvas as follows:
// load the svg
fabric.loadSVGFromURL(self.currentDraggedIcon, function(objects, d) {
var iconGroup = fabric.util.groupSVGElements(objects, d);
iconGroup.set({
left: e.layerX,
top: e.layerY,
width: d.width,
height: d.height,
lockUniScaling: true,
// scaleY:self.currentObjectDesigner.scaleFactor,
// scaleX:self.currentObjectDesigner.scaleFactor,
dtype: 'UserIcon'
});
self.currentObjectDesigner.fabric.add(iconGroup);
self.currentObjectDesigner.fabric.bringToFront(iconGroup);
self.currentObjectDesigner.fabric.renderAll();
});
Later on there is a button to say change the paths colour to red inside the group, the code to do this is:
for (var i = 0; i < self.currentObjectDesigner.selectedObject._objects.length; i++) {
if (self.currentObjectDesigner.selectedObject.item(i).fill == findColor || self.currentObjectDesigner.selectedObject.item(i).fill == findColorAlt) {
self.currentObjectDesigner.selectedObject.item(i).fill = color;
}
}
self.currentObjectDesigner.selectedObject.addWithUpdate();
This works perfectly fine if the SVG has multiple paths, but when only a single path exists the _objects property doesn't exist so we are unable to perform the loop and item(i) part to set the fill on the path.
Question is: How do we now set a fill when _objects doesn't exist and the item() method doesn't exist because it's just a single path? - I.e it's not a group.
If more than one path present groupSVGElements returns group objects else returns a single object.
DEMO
var canvas = new fabric.Canvas('c');
function loadSvg(url, left, top) {
fabric.loadSVGFromURL(url, function(objects, d) {
var iconGroup = fabric.util.groupSVGElements(objects, d);
//for more than one path
iconGroup.set({
left: left,
top: top
})
if (iconGroup.type == 'group') {
//do your logic for group object
iconGroup.item(0).fill = 'yellow';
iconGroup.addWithUpdate();
} else {
iconGroup.fill = 'red';
}
iconGroup.set({
scaleX: 150 / iconGroup.width,
scaleY: 150 / iconGroup.height,
})
canvas.add(iconGroup);
}, function() {}, {
crossOrigin: 'anonymous'
});
}
loadSvg('https://upload.wikimedia.org/wikipedia/commons/2/22/Wikimapia_logotype.svg', 10, 20);
loadSvg('https://upload.wikimedia.org/wikipedia/commons/a/a0/Circle_-_black_simple.svg', 200, 50);
canvas{
border:2px solid #000;
}
<script src="https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id='c' width=400 height=400></canvas>
Is there a way to set the width and height of an iText object? When creating the object if you set width and height it doesn't do anything. What I am after is the bounding box to be a fixed size on the canvas as an editable region, I have already extended the iText class to allow for individual character editing but I can't seem to work out how to size the box as a fixed size and allow content editing inside of it. Bare in mind that the text box can't be moved or scaled, it's static.
It's Too late for answer but other may be looking for answer.
First of all You can use TextBox class of fabricjs if you want to give fixed width & height to Text field. It is Subclass of Itext. But the problem with TextBox was it's text wrap.
So If you don't want to use TextBox but still want to have fixed width and height with Itext then following extended subclass can help you.
I have overridden the initDimensions method .
this.LimitedTextbox = fabric.util.createClass(fabric.IText, {
initDimensions: function() {
this.isEditing && this.initDelayedCursor();
this.clearContextTop();
if (this.__skipDimension) {
return;
}
this._splitText();
this._clearCache();
if (this.path) {
this.width = this.path.width;
this.height = this.path.height;
}
else {
let width = this.calcTextWidth();
if(width>this.maxWidth){
this.width = width;
}else{
this.width = this.maxWidth;
}
this.height = this.calcTextHeight();
}
if (this.textAlign.indexOf('justify') !== -1) {
this.enlargeSpaces();
}
this.saveState({ propertySet: '_dimensionAffectingProps' });
},
onKeyDown:function(e){
if (e.keyCode == 13 && this._textLines.length>=this.maxLines) {
this.exitEditing();
}
this.callSuper('onKeyDown',e);
let maxLine = Math.max(...this.__lineWidths);
self.changeBorderWidth(this.toObject().groupId, this, { iWidth:
this.width, iHeight: this.height,maxLine:maxLine,id:this.toObject().id
},false,this.toObject().customType==='stamp_input');
if(this.dynamicMinWidth>this.maxWidth){
this.width = this.dynamicMinWidth;
}
},
});
Then you can use like below
const text = new this.LimitedTextbox(txt, {
left: 100,
top: 100,
fontFamily: "arial",
angle: 0,
fontSize: 24,
fontWeight: "bold",
hasControls: true,
hoverCursor: "text",
width:250,
maxWidth: 250,
maxLines: 3,
hasRotatingPoint: true,
});
I Hope it helps , you can modify initDimensions as per your requirement , you can also see other methods in fabric's documemtation.
I would like to have svg shape scale based on text content of text area or text-input. As the text content increases, the size of the underlying svg element should increase as well
This is what I have so far:
var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
el: $('#myholder'),
width: 1330,
height: 660,
model: graph,
gridSize: 1,
defaultLink: new joint.dia.Link({
attrs: {'.marker-target': {d: 'M 10 0 L 0 5 L 10 10 z'}}
}),
validateConnection: function (cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
// Prevent linking from input ports.
if (magnetS && magnetS.getAttribute('type') === 'input')
return false;
// Prevent linking from output ports to input ports within one element.
if (cellViewS === cellViewT)
return false;
// Prevent loop linking
return (magnetS !== magnetT);
// Prevent linking to input ports.
return magnetT && magnetT.getAttribute('type') === 'input';
},
// Enable marking available cells & magnets
markAvailable: true,
//Enable link snapping within 75px lookup radius
// snapLinks: {radius: 75},
interactive: function (cellView, methodName)
{
if (cellView.model.get('isInteractive') === false)
return false;
// return true;
}
});
joint.shapes.devs.CircleModel = joint.shapes.devs.Model.extend({
markup: '<g class="rotatable"><g class="scalable"><circle class="body"/></g><text class="label"/><g class="inPorts"/><g class="outPorts"/></g>',
// portMarkup: '<g class="port port<%=1%>"><rect class="port-body"/><text class="port-label"/></g>',
defaults: joint.util.deepSupplement({
type: 'devs.CircleModel',
attrs: {
'.body': {r: 50, cx: 50, stroke: '', fill: 'white'},
'.label': {text: '', 'ref-y': 0.5, 'y-alignment': 'middle'},
'.port-body': {r: 3, width: 10, height: 10, x: -5, stroke: 'gray', fill: 'lightgray', magnet: 'active'}
}
}, joint.shapes.devs.Model.prototype.defaults)
});
joint.shapes.devs.CircleModelView = joint.shapes.devs.ModelView;
var rect = new joint.shapes.basic.Rect({
isInteractive: false,
position: {x: 10, y: 50},
size: {width: 51, height: 41},
attrs: {rect: {fill: '#D6F2FC', stroke: '#7E7E7E'}, '.': {magnet: false}}
});
// Create a custom element.
// ------------------------
joint.shapes.html = {};
joint.shapes.html.Element = 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.ElementView = joint.dia.ElementView.extend({
template: [
'<div class="html-element">',
'<button class="delete">x</button>',
'<span></span>', '<br/>',
// '<input type="text" value="" />',
'<textarea id="txt" type="text" rows="10" value="Start writing"></textarea>',
'</div>'
].join(''),
initialize: function () {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.$box = $(_.template(this.template)());
// Prevent paper from handling pointerdown.
this.$box.find('input,select').on('mousedown click', function (evt) {
evt.stopPropagation();
});
this.$ruler = $('<span>', {style: 'visibility: hidden; white-space: pre'});
$(document.body).append(this.$ruler);
// This is an example of reacting on the input change and storing the input data in the cell model.
this.$box.find('textarea').on('input', _.bind(function (evt) {
var val = $(evt.target).val();
this.model.set('textarea', val);
this.$ruler.html(val);
var width = this.$ruler[0].offsetWidth;
var height = this.$ruler[0].offsetHeight;
var area = width * height;
height = area / 150;
width = 150;
if ((area > 9000))
{
this.model.set('size', {width: width + 50, height: height + 80});
this.$box.find('textarea').css({width: width, height: height + 30});
// this.$box.find('.color-edit').css({width: width + 50, height: height + 80});
this.$box.find('.in').css({top: height + 75});
}
}, this));
this.$box.find('textarea').on('click', _.bind(function () {
this.$box.find('.delete').css({opacity: 1});
this.$box.find('textarea').css({opacity: 1});
}, this));
this.$box.find('textarea').on('blur', _.bind(function () {
this.$box.find('.delete').css({opacity: 0});
this.$box.find('textarea').css({opacity: 0});
}, this));
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();
this.listenTo(this.model, 'process:ports', this.update);
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
},
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.find('span').text(this.model.get('select'));
this.$box.css({width: bbox.width + 6, height: bbox.height, left: bbox.x, top: bbox.y, transform: 'rotate(' + (this.model.get('angle') || 0) + 'deg)'});
},
removeBox: function (evt) {
this.$ruler.remove();
this.$box.remove();
}
});
paper.on('cell:pointerdblclick', function (cellView, evt, x, y)
{
var clone = cellView.model.clone();
if (rect.id === cellView.model.id)
{
clone = new joint.shapes.html.Element({
position: {x: 100, y: 60},
size: {width: 81, height: 69},
inPorts: [''],
outPorts: [''],
attrs: {
'.': {magnet: true},
'.label': {text: '', 'ref-x': .4, 'ref-y': .2},
'.inPorts circle': {type: 'input'},
'.outPorts circle': {type: 'output'},
'.port-body': {r: 3}
}
});
// clone.resize(2*81,2*39)
graph.addCell(clone);
}
});
// // First, unembed the cell that has just been grabbed by the user.
paper.on('cell:pointerdown', function (cellView, evt, x, y) {
var cell = cellView.model;
if (!cell.get('embeds') || cell.get('embeds').length === 0) {
// Show the dragged element above all the other cells (except when the
// element is a parent).
cell.toFront();
_.invoke(graph.getConnectedLinks(cell), 'toFront');
}
if (cell.get('parent')) {
graph.getCell(cell.get('parent')).unembed(cell);
}
});
// When the dragged cell is dropped over another cell, let it become a child of the
//element below.
paper.on('cell:pointerup', function (cellView, evt, x, y) {
if (cellView.model.isLink())
return;
var cell = cellView.model;
var cellViewsBelow = paper.findViewsFromPoint(cell.getBBox().center());
if (cellViewsBelow.length) {
// Note that the findViewsFromPoint() returns the view for the `cell` itself.
var cellViewBelow = _.find(cellViewsBelow, function (c) {
return c.model.id !== cell.id;
});
// Prevent recursive embedding.
if (cellViewBelow && cellViewBelow.model.get('parent') !== cell.id) {
cellViewBelow.model.embed(cell);
}
}
});
graph.addCells([rect]);
Could not find a solution elsewhere. Any help would be appreciated. thanks
You have to make the HTML Input resize based on the text inside.
Auto-scaling input[type=text] to width of value?
The ElementView has to listen to the HTML Input changes (input event) and update the size of the model based on the width and height of the HTML Input.
Example:
function onTextInput(evt) {
var $input = $(evt.target);
// 1. auto-scaling the input based on the text inside.
$input.attr('size', Math.max($input.val().length, 10));
// 2. resizing the model to the size of the input + padding.
model.resize($input.outerWidth() + 5, $input.outerHeight() + 40);
}
$('input').on('input', onTextInput);
JS Fiddle: http://jsfiddle.net/kumilingus/Lrffgvqn/
Similar with HTML TextArea, where the only difference will be the way how you auto-scale it based on the text inside.
In fabricjs, I want to create a scene in which the object under the mouse rises to the top of the scene in z-index, then once the mouse leaves that object, it goes back to the z-index where it came from. One cannot set object.zindex (which would be nice). Instead, I'm using a placeholder object which is put into the object list at the old position, and then the old object is put back in the position where it was in the list using canvas.insertAt. However this is not working.
See http://jsfiddle.net/rFSEV/ for the status of this.
var canvasS = new fabric.Canvas('canvasS', { renderOnAddition: false, hoverCursor: 'pointer', selection: false });
var bars = {}; //storage for bars (bar number indexed by group object)
var selectedBar = null; //selected bar (group object)
var placeholder = new fabric.Text("XXXXX", { fontSize: 12 });
//pass null or a bar
function selectBar(bar) {
if (selectedBar) {
//remove the old topmost bar and put it back in the right zindex
//PROBLEM: It doesn't go back; it stays at the same zindex
selectedBar.remove();
canvasS.insertAt(selectedBar, selectedBar.XZIndex, true);
selectedBar = null;
}
if (bar) {
//put a placeholder object ("XXX" for now) in the position
//where the bar was, and put the bar in the top position
//so it shows topmost
selectedBar = bar;
canvasS.insertAt(placeholder, selectedBar.XZIndex, true);
canvasS.add(bar);
canvasS.renderAll();
}
}
canvasS.on({
'mouse:move': function(e) {
//hook up dynamic zorder
if (!e.target) return;
if (bars[e.target])
selectBar(e.target);
else
selectBar(null);
},
});
var objcount = canvasS.getObjects().length;
//create bars
for (var i = 0; i < 20; ++i) {
var rect = new fabric.Rect({
left: 0,
top: 0,
rx: 3,
ry: 3,
stroke: 'red',
width: 200,
height: 25
});
rect.setGradientFill({
x1: 0,
y1: 0,
x2: 0,
y2: rect.height,
colorStops: {
0: '#080',
1: '#fff'
}
});
var text = new fabric.Text("Bar number " + (i+1), {
fontSize: 12
});
var group = new fabric.Group([ rect, text ], {
left: i + 101,
top: i * 4 + 26
});
group.hasControls = group.hasBorders = false;
//our properties (not part of fabric)
group.XBar = rect;
group.XZIndex = objcount++;
canvasS.add(group);
bars[group] = i;
}
canvasS.renderAll();
Since fabric.js version 1.1.4 a new method for zIndex manipulation is available:
canvas.moveTo(object, index);
object.moveTo(index);
I think this is helpful for your use case. I've updated your jsfiddle - i hope this is what you want:
jsfiddle
Also make sure you change z-index AFTER adding object to canvas.
So code will looks like:
canvas.add(object);
canvas.moveTo(object, index);
Otherwise fabricjs don`t care about z-indexes you setup.
After I added a line object, I was make the line appear under the object using:
canvas.add(line);
canvas.sendToBack(line);
Other options are
canvas.sendBackwards
canvas.sendToBack
canvas.bringForward
canvas.bringToFront
see: https://github.com/fabricjs/fabric.js/issues/135
You can modify your _chooseObjectsToRender method to have the following change at the end of it, and you'll be able to achieve css-style zIndexing.
objsToRender = objsToRender.sort(function(a, b) {
var sortValue = 0, az = a.zIndex || 0, bz = b.zIndex || 0;
if (az < bz) {
sortValue = -1;
}
else if (az > bz) {
sortValue = 1;
}
return sortValue;
});
https://github.com/fabricjs/fabric.js/pull/5088/files
You can use these two functions to get z-index of a fabric object and modify an object's z-index, since there is not specific method to modify z-index by object index :
fabric.Object.prototype.getZIndex = function() {
return this.canvas.getObjects().indexOf(this);
}
fabric.Canvas.prototype.moveToLayer = function(object,position) {
while(object.getZIndex() > position) {
this.sendBackwards(object);
}
}