Scale ScrollView without breaking scrolling - javascript

Short question. How can I add a size modifier using Transitionable to a ScrollView, without breaking the scrolling itself ? Seems like it's blocking events in some way.
Code:
define('main', function (require, exports, module) {
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Transform = require('famous/core/Transform');
var Scrollview = require('famous/views/Scrollview');
var Modifier = require("famous/core/Modifier");
var Transitionable = require("famous/transitions/Transitionable");
var context = Engine.createContext();
var sizeTrans = new Transitionable(0);
var sizeModifier = new Modifier({
transform : function(){
var s = sizeTrans.get() + 1;
return Transform.scale(s, s, 0);
}
});
var scrollview = new Scrollview();
var surfaces = [];
scrollview.sequenceFrom(surfaces);
for (var i = 0; i < 40; i++) {
var surface = new Surface({
content: "Surface: " + (i + 1),
size: [undefined, 200],
properties: {
backgroundColor: "hsl(" + (i * 360 / 40) + ", 100%, 50%)",
lineHeight: "200px",
textAlign: "center"
}
});
surface.pipe(scrollview);
surfaces.push(surface);
}
context
.add(sizeModifier)
.add(scrollview);
});
I was trying to create a live example, but it's not working unfortunately :/
http://jsfiddle.net/7fzfx19h/1/

Fixed my code, now the demo works http://jsfiddle.net/7fzfx19h/2/ :
define('main', function (require, exports, module) {
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Transform = require('famous/core/Transform');
var Scrollview = require('famous/views/Scrollview');
var Modifier = require("famous/core/Modifier");
var Transitionable = require("famous/transitions/Transitionable");
var context = Engine.createContext();
var sizeTrans = new Transitionable(0);
var sizeModifier = new Modifier({
transform : function(){
var s = sizeTrans.get() + 1;
return Transform.scale(s, s);
}
});
var scrollview = new Scrollview();
var surfaces = [];
scrollview.sequenceFrom(surfaces);
for (var i = 0; i < 40; i++) {
var surface = new Surface({
content: "Surface: " + (i + 1),
size: [undefined, 200],
properties: {
backgroundColor: "hsl(" + (i * 360 / 40) + ", 100%, 50%)",
lineHeight: "200px",
textAlign: "center"
}
});
surface.pipe(scrollview);
surfaces.push(surface);
}
context.add(sizeModifier).add(scrollview);
});

Related

How to make Coin Slider responsive?

After being created, the slider seems not to have a way to be resized, which is really bad for a responsive layout.
Is there a way to actually resize the Coin Slider plugin according to Twitter Bootstrap 3's media queries?
You might consider Coin Slider's demo as a fiddle.
Indeed, there is no way to resize it with the current plugin version. So, I wrote a script to resize the Coin Slider (you can test it on Coin Slider's demo site):
var resizeCoinSliderTo = function(coinSlider, toWidth, toHeight) {
var csColumns = 7;
var csRows = 5;
var imgWidth = toWidth;
var imgHeight = toHeight;
var cellWidth = imgWidth/csColumns;
var cellHeight = imgHeight/csRows;
var coinSliderId = coinSlider.attr("id");
coinSlider.css({
'width': imgWidth,
'height': imgHeight
});
$('.cs-'+coinSliderId).css({
'width': (cellWidth + 'px'),
'height': (cellHeight + 'px'),
'background-size': (imgWidth + 'px ' + imgHeight + 'px')
}).each(function() {
var cellOffsets = $(this).attr("id").replace("cs-"+coinSliderId,"");
var hOffSet = cellHeight * (Math.floor(parseInt(cellOffsets[0])-1) % csRows);
var wOffSet = cellWidth * (parseInt(cellOffsets[1])-1);
$(this).css({
"left": (wOffSet + 'px'),
"top": (hOffSet + 'px'),
"background-position": ((-wOffSet) + 'px ' + (-hOffSet) + 'px')
});
});
$('#cs-navigation-'+coinSliderId).children("a").css("top", (((imgHeight/2)-15)+'px'));
};
So, we just need to call the created resizeCoinSliderTo after each media queries breakpoints, handling the resize, without losing its ratio, to fit the screen properly:
<span id="mq-detector">
<span class="visible-xs"></span>
<span class="visible-sm"></span>
<span class="visible-md"></span>
<span class="visible-lg"></span>
</span>
#mq-detector {
visibility: hidden;
}
var coinSlider = $("#coin-slider");
var baseWidthDisplay = undefined;
var baseHeightDisplay = undefined;
var currentRatio = undefined;
var mqRatios = [0.75, 0.95, 0.8, 1];
var mqDetector = $("#mq-detector");
var mqSelectors = [
mqDetector.find(".visible-xs"),
mqDetector.find(".visible-sm"),
mqDetector.find(".visible-md"),
mqDetector.find(".visible-lg")
];
var checkCoinSliderForResize = function() {
for (var i = 0; i <= mqSelectors.length; i++) {
if (mqSelectors[i].is(":visible")) {
if (currentRatio == undefined) {
baseWidthDisplay = parseInt(coinSlider.css("width"));
baseHeightDisplay = parseInt(coinSlider.css("height"));
}
if (i == 0) {
var specialWidth = Math.floor(parseInt($("body").css("width"))*0.75);
if (specialWidth < 300){
specialWidth = 300;
}
var specialHeight = Math.floor(baseHeightDisplay * specialWidth / baseWidthDisplay);
resizeCoinSliderTo(coinSlider, specialWidth, specialHeight);
}
if (currentRatio != mqRatios[i]) {
currentRatio = mqRatios[i];
if (i > 0) {
resizeCoinSliderTo(coinSlider, baseWidthDisplay*currentRatio, baseHeightDisplay*currentRatio);
}
}
break;
}
}
};
$(window).on('resize', checkCoinSliderForResize);
checkCoinSliderForResize();
Make sure to place all JavaScript code after the DOM is ready, and after the coinslider was created.

Piping events from ScrollContainer

I'm trying to pipe mouse clicks from ScrollContainer encapsulated in a view, but somehow nothing's happening. I've just started playing with Famo.us so maybe I'm missing something obvious here. I can pipe directly from surfaces contained within ScrollContainer but I'm not sure if that's the way to do it. Any ideas ?
Link to runnable jsFiddle demo :
http://jsfiddle.net/nqxz596f/
And the code itself :
define('main', function (require, exports, module) {
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Transform = require('famous/core/Transform');
var View = require('famous/core/View');
var Scrollview = require('famous/views/ScrollContainer');
var context = Engine.createContext();
var mainview = new View({
size: [500, 500]
});
var scrollview = new Scrollview({
scrollview: {direction:0, size: [undefined, 300]}
});
var surfaces = [];
scrollview.sequenceFrom(surfaces);
for (var i = 0; i < 40; i++) {
var surface = new Surface({
content: "Surface: " + (i + 1),
size: [undefined, 200],
properties: {
backgroundColor: "hsl(" + (i * 360 / 40) + ", 100%, 50%)",
lineHeight: "200px",
textAlign: "center"
}
});
surface.pipe(scrollview);
surfaces.push(surface);
}
mainview._eventInput.on('click', function(){
console.log('click');
});
scrollview.pipe(mainview);
mainview.add(scrollview);
context.add(mainview);
});
As of this writing, the ScrollContainer does not have it's own EventHandlers. It uses the event handlers from the ScrollView that it contains (scrollcontainer.scrollview). The ScrollContainer has a ContainerSurface that subscribes from the events of the scrollcontainer.scrollview and is the DOM element to hide the overflow of the scrollview.
The below code in a new jsFiddle shows the correct way to setup the ContainerSurface. You do not need the mainview to pipe your events.
You have a couple options:
Pipe the surface events to the scrollview event output (as below) and listen on the scrollview for the events
or Pipe the surface events to the container surface.pipe(scrollcontainer.container); and listen to the events from the container scrollcontainer.container.on('click', ...
Corrected Code:
define('main', function (require, exports, module) {
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Transform = require('famous/core/Transform');
var View = require('famous/core/View');
var ScrollContainer = require('famous/views/ScrollContainer');
var Scrollview = require('famous/views/Scrollview');
var Utility = require('famous/utilities/Utility');
var context = Engine.createContext();
var scrollcontainer = new ScrollContainer({
scrollview: {
direction: Utility.Direction.Y
}
});
scrollcontainer.container.setSize([500, 500]);
var surfaces = [];
for (var i = 0; i < 40; i++) {
var surface = new Surface({
content: "Surface: " + (i + 1),
size: [undefined, 200],
properties: {
backgroundColor: "hsl(" + (i * 360 / 40) + ", 100%, 50%)",
lineHeight: "200px",
textAlign: "center"
}
});
surface.pipe(scrollcontainer.scrollview._eventOutput);
surfaces.push(surface);
}
scrollcontainer.scrollview.on('click', function (e) {
console.log('click ' + e.target.innerText);
});
scrollcontainer.sequenceFrom(surfaces);
context.add(scrollcontainer);
});
Alternative Option:
for (var i = 0; i < 40; i++) {
var surface = new Surface({
content: "Surface: " + (i + 1),
size: [undefined, 200],
properties: {
backgroundColor: "hsl(" + (i * 360 / 40) + ", 100%, 50%)",
lineHeight: "200px",
textAlign: "center"
}
});
surface.pipe(scrollcontainer.container);
surfaces.push(surface);
}
scrollcontainer.container.on('click', function (e) {
console.log('click ' + e.target.innerText);
});
jsFiddle to show event pipe to a View
for (var i = 0; i < 40; i++) {
var surface = new Surface({
content: "Surface: " + (i + 1),
size: [undefined, 200],
properties: {
backgroundColor: "hsl(" + (i * 360 / 40) + ", 100%, 50%)",
lineHeight: "200px",
textAlign: "center"
}
});
surface.pipe(scrollcontainer.container);
surfaces.push(surface);
}
var mainview = new View({size: [500, 500]});
mainview.on('click', function (e) {
console.log('click ' + e.target.innerText);
});
scrollcontainer.sequenceFrom(surfaces);
scrollcontainer.container.pipe(mainview._eventOutput);
context.add(scrollcontainer).add(mainview);

Spring between two draggable surfaces in famo.us

I'm trying to implement two surfaces, connected with a spring, that would react to drag in famo.us. So far I have setup the surfaces, can drag those, have a spring that interacts during the loading of the page, but not on drag. So the questions are a) how should I connect two surfaces with a spring and b) how do I update the physics when I drag one surface so that the other surface would follow the dragged surface?
The code I so far have is this
define(function(require) {
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var StateModifier = require('famous/modifiers/StateModifier');
var PhysicsEngine = require('famous/physics/PhysicsEngine');
var Circle = require('famous/physics/bodies/Circle');
var Draggable = require('famous/modifiers/Draggable');
var Spring = require('famous/physics/forces/Spring');
var Vector = require('famous/math/Vector');
var context = Engine.createContext();
var physicsEngine = new PhysicsEngine();
var ball = new Surface ({
size: [100,100],
properties: {
backgroundColor: 'red',
borderRadius: '50px'
}
});
var ball2 = new Surface ({
size: [100,100],
properties: {
backgroundColor: 'blue',
borderRadius: '50px'
}
});
var draggable = new Draggable();
var draggable2 = new Draggable();
ball.state = new StateModifier({origin:[0.2,0.2]});
ball2.state = new StateModifier({origin:[0.3,0.3]});
ball.particle = new Circle({radius:100});
ball2.particle = new Circle({radius:100});
var spring = new Spring({
anchor: ball.particle,
period: 400, // <= Play with these values :-)
dampingRatio: 0.07, // <=
length: 50
});
// var spring2 = new Spring({anchor: ball2.particle});
// physicsEngine.attach(spring, ball2.particle);
// physicsEngine.attach(spring2, ball.particle);
draggable.subscribe(ball);
draggable2.subscribe(ball2);
draggable.on('update', function() {
console.info('update');
ball2.particle.applyForce(new Vector(0, 0, -0.005 * 100));
// ball.state.setTransform(ball.particle.getTransform())
// ball.state.setTransform(ball.particle.getTransform())
// ball.particle.setVelocity([0.001,0,0]);
// physicsEngine.wake();
// physicsEngine.step();
});
draggable2.on('update', function() {
// ball2.particle.setVelocity([0.001,0,0]);
// console.info('update');
// physicsEngine.wake();
// physicsEngine.step();
});
physicsEngine.attach(spring, ball2.particle);
// spring.applyForce(ball.particle);
physicsEngine.addBody(ball.particle);
physicsEngine.addBody(ball2.particle);
// ball.on("click",function(){
// ball.particle.setVelocity([10,0,0]);
// });
//
// ball2.on("click",function(){
// ball2.particle.setVelocity([0,10,0]);
// });
context.add(draggable).add(ball.state).add(ball);
context.add(draggable2).add(ball2.state).add(ball2);
Engine.on('prerender', function(){
ball.state.setTransform(ball.particle.getTransform());
ball2.state.setTransform(ball2.particle.getTransform());
});
});
It seems like you have a pretty good understanding of the PE thus far. I can still see a few places you can improve. Here is a working example of dragging with a spring attached. Although this implementation is not perfect yet, it should get you started.. If you start with dragging the red circle, everything works as expected.. Draggable has its own position, and so does particle. So when you grab the blue circle, there remains an offset in particle. Here is what will get you 95%..
Hope it helps..
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Transform = require('famous/core/Transform');
var Modifier = require('famous/core/Modifier');
var Draggable = require('famous/modifiers/Draggable');
var PhysicsEngine = require('famous/physics/PhysicsEngine');
var Circle = require('famous/physics/bodies/Circle');
var Spring = require('famous/physics/forces/Spring');
var context = Engine.createContext();
var physicsEngine = new PhysicsEngine();
var ball = new Surface ({
size: [100,100],
properties: {
backgroundColor: 'red',
borderRadius: '50px'
}
});
var ball2 = new Surface ({
size: [100,100],
properties: {
backgroundColor: 'blue',
borderRadius: '50px',
}
});
ball.mod = new Modifier({origin:[0.5,0.5]});
ball.draggable = new Draggable();
ball.pipe(ball.draggable);
ball.particle = new Circle({radius:100});
ball.mod.transformFrom(function(){ return Transform.translate(0,0,0) });
ball.spring = new Spring({
anchor: ball.particle,
period: 400,
dampingRatio: 0.07,
length: 50
});
ball2.mod = new Modifier({origin:[0.5,0.5]});
ball2.draggable = new Draggable();
ball2.pipe(ball2.draggable);
ball2.particle = new Circle({radius:100});
ball2.mod.transformFrom(function(){ return ball2.particle.getTransform()});
ball2.spring = new Spring({
anchor: ball2.particle,
period: 400,
dampingRatio: 0.07,
length: 50
});
ball.draggable.on('start',function(){
ball2.setProperties({pointerEvents:'none'});
if (ball2.springID) physicsEngine.detach(ball2.springID);
if (ball.springID) physicsEngine.detach(ball.springID);
ball.springID = physicsEngine.attach(ball.spring, ball2.particle);
ball2.springID = null;
ball.mod.transformFrom(function(){ return Transform.translate(0,0,0) });
ball2.mod.transformFrom(function(){ return ball2.particle.getTransform()});
})
ball.draggable.on('update', function() {
pos = ball.draggable.getPosition();
ball.particle.setPosition(pos);
});
ball.draggable.on('end', function() {
ball2.setProperties({pointerEvents:'all'});
});
ball2.draggable.on('start',function(){
ball.setProperties({pointerEvents:'none'});
if (ball2.springID) physicsEngine.detach(ball2.springID);
if (ball.springID) physicsEngine.detach(ball.springID);
ball2.springID = physicsEngine.attach(ball2.spring, ball.particle);
ball.springID = null;
ball2.mod.transformFrom(function(){ return Transform.translate(0,0,0) });
ball.mod.transformFrom(function(){ return ball.particle.getTransform()});
})
ball2.draggable.on('update', function() {
pos = ball2.draggable.getPosition();
ball2.particle.setPosition(pos);
});
ball2.draggable.on('end', function() {
ball.setProperties({pointerEvents:'all'});
});
ball.springID = physicsEngine.attach(ball.spring, ball2.particle);
physicsEngine.addBody(ball.particle);
physicsEngine.addBody(ball2.particle);
context.add(ball.mod).add(ball.draggable).add(ball);
context.add(ball2.mod).add(ball2.draggable).add(ball2);

Bind events to all elements in class instead of just to one id

I developed this interaction / script that scales whatever element is passed to it and if that element is pinched in on, it scales down / less.
This is how the script is initialised ( passing two arguments the container and the item to be scaled / transformed:
$(function(){
var zoom = new collapse('#zoom','#zoom :first');
var zoom2 = new collapse('#zoom2','#zoom2 :first');
var zoom3 = new collapse('#zoom3','#zoom3 :first');
});
It works fine as above on single IDs, but I need it to work on a class.
I tried this:
$(function(){
var zoom = new collapse('#zoom','.polaroid');
});
But that causes the whole script not to work because all the elements in that class are being passed instead of one as with an id.
This would only select the first item in the class so it won't work:
$(function(){
var zoom = new collapse('#zoom','.polaroid :first');
});
How can I change my script so that it is applied to all members of the .polaroid class in the #main container?
Here is my script:
function collapse(container, element){
container = $(container).hammer({
prevent_default: true,
scale_threshold: 0
});
element = $(element);
var displayWidth = container.width();
var displayHeight = container.height();
var MIN_ZOOM = 0;
var MAX_ZOOM = 1;
var scaleFactor = 1;
var previousScaleFactor = 1;
var startX = 0;
var startY = 0;
var translateX = 0;
var translateY = 0;
var previousTranslateX = 0;
var previousTranslateY = 0;
var time = 1;
var tch1 = 0,
tch2 = 0,
tcX = 0,
tcY = 0,
toX = 0,
toY = 0,
cssOrigin = "";
container.bind("transformstart", function(event){
e = event;
tch1 = [e.touches[0].x, e.touches[0].y],
tch2 = [e.touches[1].x, e.touches[1].y];
tcX = (tch1[0]+tch2[0])/2,
tcY = (tch1[1]+tch2[1])/2;
toX = tcX;
toY = tcY;
var left = $(element).offset().left;
var top = $(element).offset().top;
cssOrigin = (-(left) + toX)/scaleFactor +"px "+ (-(top) + toY)/scaleFactor +"px";
});
container.bind("transform", function(event){
scaleFactor = previousScaleFactor * event.scale;
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
transform(event);
});
container.bind("transformend", function(event){
previousScaleFactor = scaleFactor;
if(scaleFactor > 0.42){
$(element).css('-webkit-transform', 'scaleY(1.0)').css('transform', 'scaleY(1.0)');
}
});
function transform(e){
var cssScale = "scaleY("+ scaleFactor +")";
element.css({
webkitTransform: cssScale,
webkitTransformOrigin: cssOrigin,
transform: cssScale,
transformOrigin: cssOrigin,
});
if(scaleFactor <= 0.42){
$(element).animate({height:0}, function(){
$(this).remove();
});
}
}
}
Wrap it as a jquery plugin:
$.fn.collapse = function(filter) {
return this.each(function(){
collapse(this,filter);
});
}
$("#zoom,#zoom1,#zoom2").collapse(".polaroid");
or if each of the zoom elements had a common class,
$(".zoomel").collapse(".polaroid");
You have to run collapse for each element.
element = $(element);
element.each(function(){
//each element would be this here
var $this= $(this);
//do whatever you want with $this
})

Combining two control panels(AB) or toggle them (A) or (B)

I have two control panels; one is for the default draw features, the other for measure tools.
The problem is that since they are in different panels it is possible to run two controls simultaneously, one from each panel. (this didn't seem like much of a problem before, but I noticed that when the measure and default draw tools are activated together, they cancel out the line end function)
What I am trying to do is either place all controls in one panel and toggle them together or toggle the panels(e.g. when control from panel 1 is activated, deactivate all controls in panel 2)
Here is my code:
Default Controls Panel:
OpenLayers.Control.CustomNavToolbar = OpenLayers.Class(OpenLayers.Control.Panel,{
initialize: function(options){
OpenLayers.Control.Panel.prototype.initialize.apply(this, [options]);
this.addControls([
new OpenLayers.Control.Navigation({displayClass: 'olControlNavigation', zoomBoxEnabled:false}),
new OpenLayers.Control.DrawFeature(vlayer, OpenLayers.Handler.Point, {displayClass: 'olControlDrawPoint'}),
new OpenLayers.Control.DrawFeature(vlayer, OpenLayers.Handler.Path, {displayClass: 'olControlDrawPath'}),
new OpenLayers.Control.DrawFeature(vlayer, OpenLayers.Handler.Polygon, {displayClass: 'olControlDrawPolygon'}),
new OpenLayers.Control.ZoomBox({displayClass: 'olControlZoomBox', alwaysZoom:true})
])
this.displayClass = 'olControlCustomNavToolbar'
},
draw: function(){
var div = OpenLayers.Control.Panel.prototype.draw.apply(this, arguments);
this.defaultControl = this.controls[0];
return div;
}
});
var panel = new OpenLayers.Control.CustomNavToolbar({div:OpenLayers.Util.getElement('panel')});
map.addControl(panel);
Measure Controls Panel:
allControls = {
line: new OpenLayers.Control.Measure(OpenLayers.Handler.Path, {
persist: true,
handlerOptions: {
layerOptions: {
renderers: renderer,
styleMap: styleMap
}
},
textNodes: null,
callbacks:{
create:
function(){
this.textNodes = [];
// vlayer.destroyFeatures(vlayer.features);
mouseMovements = 0;
},
modify:
function(point, line){
if(mouseMovements++ < 5){
return;
}
var len = line.geometry.components.length;
var from = line.geometry.components[len -2];
var to = line.geometry.components[len -1];
var ls = new OpenLayers.Geometry.LineString([from, to]);
var dist = this.getBestLength(ls);
if(!dist[0]){
return;
}
var total = this.getBestLength(line.geometry);
var label = dist[0].toFixed(3) + " " + dist[1];
var textNode = this.textNodes[len -2] || null;
if(textNode && !textNode.layer){
this.textNodes.pop();
textNode = null;
}
if(!textNode){
var c = ls.getCentroid();
textNode = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(c.x, c.y), {}, {
label: "",
fontColor: "#800517",
fontSize: "13px",
fontFamily: "Tahoma",
fontWeight: "bold",
labelAlign: "cm"
});
this.textNodes.push(textNode);
vlayer.addFeatures([textNode]);
}
textNode.geometry.x = (from.x + to.x) / 2;
textNode.geometry.y = (from.y + to.y) / 2;
textNode.style.label = label;
textNode.layer.drawFeature(textNode);
this.events.triggerEvent("measuredynamic", {
measure: dist[0],
total: total[0],
units: dist[1],
order: 1,
geometry: ls
});
}
}
}),
polygon: new OpenLayers.Control.Measure(
OpenLayers.Handler.Polygon, {
persist: true,
immediate: true,
handlerOptions: {
layerOptions: {
renderers: renderer,
styleMap: styleMap
}
}
}
)
};
var control;
for(var key in allControls) {
control = allControls[key];
control.events.on({
"measure": handleMeasurements,
"measurepartial": handleMeasurements
});
map.addControl(control);
}
..and just for reference
function handleMeasurements(evt){
var geometry = evt.geometry;
var units = evt.units;
var order = evt.order;
var measure = evt.measure;
var element = document.getElementById('output');
var position = (map.getLonLatPxFromViewPortPx);
var out = "";
if(order == 1){
out += "Distance: " + measure.toFixed(3) + " " + units;
}
else{
out += "Area: " + measure.toFixed(3) + " " + units + "<sup>2</" + "sup>";
}
element.innerHTML = out;
}
function toggleControl(element){
vlayer.destroyFeatures(vlayer.features);
for(key in allControls){
var control = allControls[key];
if(element.value == key && element.click){
control.activate();
var label = document.getElementById('output');
var emptyOut = "";
label.innerHTML = emptyOut;
}
else{
control.deactivate();
}
}
}
Been looking for a way to do this and so far have not been able to find anything useful, if you could help me with some suggestions about how to go about this it would be very appreciated. Thanks
EDIT: I am not using GeoExt and am not considering using it. I'm looking for suggestions about how to go about this using only the OpenLayers 2.11 library. Thanks again.

Categories

Resources