Is there a way to get the paper for an element by referencing the element?
I'm creating elements in a loop and with each element i'm creating a new Raphael(...). See sample below.
Basically I want to stop the animation on click, but paper is undefined and calling stop() on the element itself doesn't work either.
$.each(el,function(key,value)
{
var li = $("<li>",{id:"item"+key).appendTo("#myUl");
var ppr = new Raphael($("item"+key),get(0),48,48);
//... do stuff like animate ...
li.click(function()
{
console.log($(this).paper); //undefined
})
})
I was wondering about a closure like below to capture the paper, so when the anonymous func runs, it has the variable captured.
Note, I'm not sure this is the best method overall, something feels a bit clunky about creating a new paper each time, but just trying to address the specific issue.
Untested code, but if you can get it on a fiddle, I think it should be possible to sort.
$.each(el,function(key,value)
{
var li = $("<li>",{id:"item"+key).appendTo("#myUl");
var ppr = new Raphael($("item"+key),get(0),48,48);
(function() {
var myPaper = ppr;
li.click(function()
{
console.log(myPaper);
})
})();
})
You can also attach the paper to the element's "data" using https://api.jquery.com/data/
$.each(el,function(key,value)
{
var li = $("<li>",{id:"item"+key).appendTo("#myUl");
var ppr = new Raphael($("item"+key),get(0),48,48);
li.data("paper", ppr ); // SAVE
li.click(function()
{
console.log($(this).data("paper")); // LOAD
})
})
Related
In the following example code, a SwiftUI form holds an Observable object that holds a trivial pipeline that passes a string through to a #Published value. That object is being fed by the top line of the SwiftUI form, and the output is being displayed on the second line.
The value in the text field in the first row gets propagated to the output line in the second row, whenever we hit the "Send" button, unless we hit the "End" button, which cancels the subscription, as we'd expect.
import SwiftUI
import Combine
class ResetablePipeline: ObservableObject {
#Published var output = ""
var input = PassthroughSubject<String, Never>()
init(output: String = "") {
self.output = output
self.input
.assign(to: &$output)
}
func reset()
{
// What has to go here to revive a completed pipeline?
self.input
.assign(to: &$output)
}
}
struct ResetTest: View {
#StateObject var pipeline = ResetablePipeline()
#State private var str = "Hello"
var body: some View {
Form {
HStack {
TextField(text: $str, label: { Text("String to Send")})
Button {
pipeline.input.send(str)
} label: {
Text("Send")
}.buttonStyle(.bordered)
Button {
pipeline.input.send(completion: .finished)
} label: {
Text("End")
}.buttonStyle(.bordered)
}
Text("Output: \(pipeline.output)")
Button {
pipeline.reset()
} label: {
Text("Reset")
}
}
}
}
struct ResetTest_Previews: PreviewProvider {
static var previews: some View {
ResetTest()
}
}
My understanding is that hitting "End" and completing/cancelling the subscription will delete all the Combine nodes that were set up in the ResetablePipeline.init function (currently only the assign operator).
But if we wanted to reset that connection, how would we do that (without creating a new ResetablePipeline object). What would you have to do in reset() to reconnect the plumbing in the ResetablePipeline object, so that the Send button would work again? Why does the existing code not work?
It is part of the fundamental nature of a Publisher that once the Publisher has finished, or has emitted an error, that the publisher will never emit another value.
This is described in Reactive X in the Observable Contract
The fundamental reason for this is that when the pipeline finishes, the stages in the pipeline are free to release any resources they may have obtained. For example, if a collect operator has set aside memory for its connected items, it can release that memory once the pipeline finishes.
In short, there is no way to do what you want to do. You cannot restart a pipeline that has finished, though you can construct a new one.
Well I'll be. If I simply add input = PassthroughSubject<String, Never>() to the start of reset() (ie replace the original cancelled head-publisher with a fresh one), it seems to do the trick.
Now, I'm not entirely sure if this code is not leaking something since I don't know exactly what assign(to:) does with the old subscription, but assuming that it's sensible, this might be OK.
Can anyone see anything wrong with this approach?
Please see the code below. Pressing the button once (or twice at most) is almost certain to crash the app. The app shows a list containing two sections, each of which have four items. When button is pressed, it inserts a new item into each section and also changes the section order.
I have just submitted FB9952691 to Apple. But I wonder if anyone on SO happens to know 1) Does UIKit has the same issue? I'm just curious (the last time I used UIkit was two years ago). 2) Is it possible to work around the issue in SwiftUI? Thanks.
import SwiftUI
let groupNames = (1...2).map { "\($0)" }
let groupNumber = groupNames.count
let itemValues = (1...4)
let itemNumber = itemValues.count
struct Item: Identifiable {
var value: Int
var id = UUID()
}
struct Group: Identifiable {
var name: String
var items: [Item]
var id = UUID()
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
}
struct Data {
var groups: [Group]
// initial data: 2 sections, each having 4 items.
init() {
groups = groupNames.map { name in
let items = itemValues.map{ Item(value: $0) }
return Group(name: name, items: items)
}
}
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
}
struct ContentView: View {
#State var data = Data()
var body: some View {
VStack {
List {
ForEach(data.groups) { group in
Section {
ForEach(group.items) { item in
Text("\(group.name): \(item.value)")
}
}
header: {
Text("Section \(group.name)")
}
}
}
Button("Press to crash the app!") {
withAnimation {
data.change()
}
}
.padding()
}
}
}
More information:
The error message:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates (oldRow=8, oldGlobalRowCount=8)'
The issue isn't caused by animation. Removing withAnimation still has the same issue. I believe the issue is caused by the section order change (though it works fine occasionally).
Update: Thank #Yrb for pointing out an out-of-index bug in insertItem(). That function is a setup utility in the example code and is irrelevant to the issue with change(). So please ignore it.
The problem is here:
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
You are attempting to do too much to the array at once, so in the middle of reversing the order, the array counts are suddenly off, and the List (and it's underlying UITableView) can't handle it. So, you can either reverse the rows, or add an item to the rows, but not both at the same time.
As a bonus, this will be your next crash:
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
though it is not causing the above as I fixed this first. You have set a fixed Int for itemNumber which is the count of the items in the first place. Arrays are 0 indexed, which means the initial array indices will be (0...3). This line let index = (0...itemNumber).randomElement()! gives you an index that is in the range of (0...4), so you have a 20% chance of crashing your app each time this runs. In this sort of situation, always use an index of (0..<Array.count) and make sure the array is not empty.
I got Apple's reply regarding FB9952691. The issue has been fixed in iOS16 (I verified it).
Issue:
my famo.us app allows a user to add many "card" views on a work area. These cards are draggable, added directly to a ContainerSurface, and not in any particular layout.
The cards have a delete button, which when pressed deletes the card.
Currently I "delete" the view using
theCardView.render = function(){ return null; }
This works in removing the view and the surfaces on it from the work area;however, it has a strange side-effect. When I add another card the delete button surface is under the header surface that it normally sits on.
If I add a 2nd card, then the delete button on this 2nd card is displayed properly.
When I look at the DOM after the delete I see that divs for the surfaces on my view are still there, but empty
Is there a better/cleaner way to "delete" a view? Or is there something else I need to do to ensure the DOM is cleaned up?
I can get around this issue by setting the z-index of the button to 1; however this causes a problem when you drag the cards around over each other - The button is always on top of all the other cards.
Code of how I add the CardViews
this._eventInput.on('add-cards', function() {
for (var i=0; i < that.cardPicker.selected.length ;i++){
var x = new CardView({data:that.cardPicker.selected[i]});
that.subscribe(x);
that.cards.push(x);
that.contentContainer.add(x);
}
});
Code of how I delete the view
this._eventInput.on('delete-card',function(e){
// e is the CardView object that has the button that was clicked
removeFromArray(that.cards,e);
e.render = function(){ return null; }
});
Code for the view constructor
function CardView(data) {
View.apply(this, arguments);
that = this;
this.dbdata = data.data;
this.layout = new HeaderFooterLayout({
headerSize: 43,
footerSize: 0
});
// Header
var oname = this.dbdata.dbname+"."+this.dbdata.table.name;
this.headerSurface = new Surface({size: [275, 43],
content: oname
,properties: {
backgroundColor:'rgb(51, 51, 51)'
,color:'white'
,paddingTop:'10px'
,paddingLeft:'10px'
}
});
this.draggable = new Draggable();
this.headerSurface.pipe(this.draggable);
this.draggable.activate();
this.layout.header.add(this.headerSurface);
// Header delete button
this.deletebtn = new Surface({size: [55, 40],
content: '<button type="button" class="btn btn-default">del</button>'
,properties:{
}
});
this.mod = new Modifier({
transform: Transform.translate(215, 0, 0)
});
this.layout.header.add(this.mod).add(this.deletebtn);
this.deletebtn.pp = this;
this.deletebtn.on('click', function() {
that._eventOutput.emit('delete-card',this.pp);
});
this.layout.content.add(new Surface({size: [275, 300],
properties: {
backgroundColor:'rgb(236, 236, 236)'
}
}));
// a modifier that centers the surface
this.centerModifier = new Modifier({
origin : [0.5, 0.5],
align: [0.5, 0.5]
});
this.add(this.draggable).add(this.centerModifier).add(this.layout);
}
So a couple of things:
Deleting surfaces from the context
When calling context.add() a RenderNode is created and returned, all subsquent views/modifiers/etc added to this RenderNode create additional nodes. The render node is responsible for reporting to the context what elements it contains (each surface/modifier/etc is responsible for reporting what it represents).
To clear all of the elements from a render node do something like the following
var myView = new View();
var context = Engine.createContext();
myView.node = context.add(myView); // Now the context knows about your view
myView.node.set({}); // Now the context doesn't
Now the problem remains that the parent node/context will maintain a reference to the original node even though it doesn't contain anything. This is something that is known and are working towards a solution.
Dealing with z-index
If you wanted to set the z-index of the button so that it is above the surface itself, you can also modify the z-index of the focused view. This can be accomplished by maintaining a reference to the modifier of the specific view and increasing the z-index. You will have to ensure the the change in z-index flows down into each element.
I would go for the first solution and delete the elements from the render node.
I have a loop that creates surfaces from "TribesView" and pipes them into a scroll view, the loop also creates a "ProductView" with a modifier and adds this to a context. I want to bind an event to the "ProductView" surface where if the surface is clicked then set the opacity to '1'. What I have so far only sets the opacity to the last "ProductView" in the array regardless of which "TribeView" is clicked. Here is the code:
for (var t = 0; t < tribesLength; t++) {
var tribe = new TribesView({tribes: tribes, tribe: t});
var tribeProduct = new ProductView({tribes: tribes, tribe: t});
var productModifier = new StateModifier({
opacity: '0'
});
tribe.on('click', function() {
productModifier.setOpacity(1)
});
productContext.add(productModifier).add(tribeProduct);
tribe.pipe(scrollView);
surfaces.push(tribe);
}
Yes, you want to bind the productModifier to the function. When you bind an object, it becomes represented as 'this' within the function.
Here is how it should be done.. Good luck!
tribe.on('click', function() {
this.setOpacity(1);
}.bind(productModifier));
I've just moved to Famo.us and think it has some amazing potential. I am trying to build a new App using Famo.us and will be having 'layered' Views, one of which has a CanvasSurface inside.
My question is about how I would populate the CanvasSurface? I have checked the Docs and while they talk about some of the parameter options they do not tell you how.
I have a View within which I add a Layout and then a Surface to that Layout. Other Views that have ImageSurfaces work fine - but I do not know if I am on the right track with CanvasSurface.
So far I have: (part of inside a BackgroundView.js file)
function BackgroundView() {
View.apply(this, arguments);
_createLayout.call(this);
_createBody.call(this);
_setListeners.call(this);
}
function _createBody() {
this.bodySurface = new CanvasSurface({
canvasSize : [undefined, undefined]
});
var bodyContext= this.bodySurface.getContext('2d');
bodyContext.fillText("Text on Canvas", 100, 100);
this.layout.content.add(this.bodySurface);
}
It runs with no errors, but shows nothing. The CanvasSurface is rendered...
Are there any examples using CanvasSurface or does anyone have any thoughts?
Thanks again for your help in advance.
:)
There are a couple of things I added to your code.. Defining the prototype and prototype.constructor, as well as adding the CanvasSurface to the BackgroundView. I found that canvasSize does not currently support the undefined size attribute like Surface does. You need to be explicit and use pixel size.
Check out what I did to your code.. Hope this helps..
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var View = require('famous/core/View');
var CanvasSurface = require('famous/surfaces/CanvasSurface');
var context = Engine.createContext();
function BackgroundView() {
View.apply(this, arguments);
// _createLayout.call(this);
_createBody.call(this);
// _setListeners.call(this);
}
BackgroundView.prototype = Object.create(View.prototype);
BackgroundView.prototype.constructor = BackgroundView;
function _createBody() {
this.bodySurface = new CanvasSurface({
size:[400,200]
});
var bodyContext= this.bodySurface.getContext('2d');
bodyContext.font="20px Georgia";
bodyContext.fillText("Hello World!",50,50);
this.add(this.bodySurface);
}
var bg = new BackgroundView();
context.add(bg);