I'm struggling to come up with a clean solution for a design that enables a class to do something when something has happened in another class. I've been looking at Promises, callbacks and events but the application of Promises and events hasnt stuck yet.
My design issue is that I want a user to click on a button, which opens a filemanager (basically a popup with lots if images to select) and then update a thumbnail on the page when the user has selected an image from the filemanager.
I have a class that handles the controls on the main page which creates the filemanager class and opens it.
I want the main class to respond to the selected image and update the widget - I want the filemanager to just be responsible for selections not updating the main page.
So, how do I communicate between the classes so that the main class gets the selected image and does the update?
I've looked at loads of tutorials but confused on the implementation in this case.
Also - do events only relate to DOM elements. For example could I create a custom event to fire once a selection is made? confused.com
here#s the bones of my classes (removed some elements to keep brief):
export let fileImageWidgetControls = class {
/**
* sets up buttons
* sets up control actions
* opens filemanager
* handles update image
*
* */
options = {
scope: ['file', 'image'],
addBtns: {
image: '#add-image',
file: '#add-file'
},
editBtns: {
image: [
'.preview-image',
'#replace-image',
'#remove-image'
],
file: [
'.preview-file',
'#replace-file',
'#remove-file'
]
}
};
constructor() {
this.imageWidget = new updateWidget;
this.imageControls = new ImageControls(this.options);
this.initialiseControls();
this.observer();
}
openFileManager = () => {
const filemanager = new filemanagerHandler({
images:true
});
filemanager.open();
/**HOW WILL THIS CLASS KNOW THAT AN IMAGE HAS BEEN SELECTED?**/
let selectedItem = filemanager.getSelectedAsset();
}
/**
* select image via filemanager
*/
select = () => {
this.openFileManager();
};
initialiseControls = () => {
const module = this;
this.options.scope.forEach((scope) => {
//add
$(this.options.addBtns[scope]).on('click', (e, scope)=> {
e.preventDefault();
module.select(scope);
});
});
}
and here's the filemanager class (again trimmed down to relevant parts):
export let filemanagerHandler = class {
constructor({
images = false, //show image panel
files = false, //hide files panel
upload = true, //show upload panel
serverURL = '/filemanager',
baseUrl = 'http://site.s3.amazonaws.com'
} = {}) {
this.options = {
activeTabs: {
images: images,
files: files,
upload: upload
},
serverURL: serverURL,
baseURL: baseUrl
}
}
/**
* set up filemanager panels and controls
*/
init = () => {
this.imagePreviewModal = $('#imagePreview3');
this.modalSelectBtn = $('#modalSelectBtn');
this.tabClick();
this.setUpPanels(this.options.activeTabs);
this.setUpControls();
this.uploadHander = new uploadHandler;
this.observer();
this.setUpEvents();
}
open = ()=> {
colBox.init({
href: this.options.serverURL
});
colBox.colorbox();
}
/**
* set up controls within filemanager form (once loaded)
*/
setUpControls = () => {
let module = this;
$('.select-image').on('click', function (e) {
e.preventDefault();
module.handleSelection($(this), 'img');
})
}
/**
* close colorbox
* selection then handled from within bind colbox closed
* in calling class
*/
closeColorbox() {
colBox.closeColorbox();
}
setUrl = (url) => {
this.options.serverURL = url;
}
/**
* get the properties of the selected file
* from the element and updates selectedAsset attribute
* #param element (selected element)
* #param type (image or file
* #param callback
*/
handleSelection = (element, type, callback) => {
//set up selected element to be used by calling method in calling class
this.selectedAsset = {
filename: element.attr('href'),
id: element.data('id'),
caption: element.data('caption'),
type: type
}
/** HOW CAN I GET THE CALLING CLASS TO RESPOND TO THIS EVENT/SELECTION?**/
/** IVE THOUGHT ABOUT A CALLBACK HERE BUT FEELS CLUMSY ?**/
callback();
}
getSelectedAsset = () => {
return (this.selectedAsset === undefined ? false : this.selectedAsset);
}
setUpEvents = () =>{
//watch for colorbox to complete and initiate controls in the open document
$(document).bind('cbox_complete', () => {
this.init();
});
}
};
Related
I have an application that has multiple "widgets" that can be dragged and dropped onto the widget preview. Each widget has its own class and I am looking for a way to pass these classes into the drag and drop events.
Currently on the dragstart event I am passing the typeof the widget class and converting it to a string:
const addWidget = (widgetType) => {
// widgetBox is the box dragged onto the widget preview to specify the user wants to create this widget
const widgetBox = document.createElement("div");
// style widgetBox
widgetBox.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("widget-type", String(widgetType));
})
}
addWidget(TextboxWidget); // TextboxWidget is a class
On the drop event in the widget preview I then get the widget type string and am forced to manually check for each widget, which is not ideal:
element.addEventListener("drop", () => {
e.preventDefault();
const widgetType = e.dataTransfer.getData("widget-type");
switch(widgetType) {
case String(TextboxWidget):
widget = new TextboxWidget();
break;
// etc for each widget
default:
return;
}
})
Ideally I would like to be able to pass the widget class to the drop event, and then be able to create an instance of it like so:
// dragover
const addWidget = (widget) => {
const widgetBox = document.createElement("div");
widgetBox.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("widget", widget);
})
}
addWidget(TextboxWidget);
// drop
element.addEventListener("drop", () => {
e.preventDefault();
const WidgetClass = e.dataTransfer.getData("widget");
const widgetInstance = new WidgetClass();
I'm trying to create a vanilla JavaScript Modal that has the capability of being customized by the User, when instantiating it from the HTML file (or JS file). However, when it comes to dealing with the close() function to close the modal, instead of closing ONE modal at a time, using its close button, the close button of the FIRST modal closes ALL modals of the page. I'm not sure what I'm doing wrong...
I've researched other similar vanilla JavaScript, customizable modal libraries, but most of them use either jQuery, some framework, or include a lot of complications that I am not familiar with (I'm still a beginner). I've researched on GitHub, CodePen, Google, and on here; but I have yet to find a solution that satisfies what I need.
Since the code is quite long, I'd suggest you go directly to my CodePen account, where you can have the full code.
https://codepen.io/jdriviere/pen/zYOyJvv?editors=0011
But here is my close() function:
Modal.prototype.close = function() {
let modal = document.getElementById(this.options.id);
let modalBody = modal.children[0];
// Delete elements from Modal Body
for (let i = 0; i < modalBody.children.length; i++) {
modalBody.removeChild(modalBody.children[i]);
} // End of LOOP
// Delete Modal Body from Modal
modal.removeChild(modalBody);
// Delete Modal from DOM
modal.style.display = 'none';
document.body.removeChild(modal);
return this;
};
I would expect the code to close ONE modal at a time, and preferably the modal that has the proper ID (which should be either assigned by the User or by default have a "NoID" ID). Instead, if I close subsequent modals, it closes them; but if I close the FIRST one, it closes ALL of them. Also, is there a way to init() the Modal functionalities as soon as you create the modal instance (I hate manually initiating them)? If so, please include your solution here too, if not much asked.
Been at it for quite some time now. Your help would be greatly appreciated.
Thank you. :)
You have couple of mistakes in your code:
Always use a proper id pattern for the HTML element. You have used n/a for the modal that does not have id property in their options object. Using such id will break the query selector when you use jQuery.
Since, you are calling the init() function twice and in each call for init() the closeBtn is selecting both the close buttons of two modals and assigning the click event handler to each of them twice. That was the reason when you clicked on one button the click event for another button was executing itself. So, what you can do is, only associate a click function once only to that close button of the modal for which the init() function was called. I used let closeBtn = document.querySelector('#'+this.options.id + ' .modal-close'); to select that particular close button inside that init() function.
Overall your JS code will look like:
/**
* Blueprint function (class) that describes a Modal object.
* #param {Object} options Object parameter containing elements that describe the Modal.
* #returns {Object} options Returns options from current modal object.
*/
function Modal(options) {
// If constructor params is available
if (options) {
this.options = options;
} else {
this.options = {};
} // End of IF-ELSE
// Add to options object
if (options.id) {
// Check type of ID entry
if (typeof options.id === 'number') {
this.options.id = options.id.toString();
} else {
this.options.id = options.id;
} // End of IF-ELSE
} else if (options.id === undefined) {
this.options.id = 'NA';
} // End of IF-ELSE
if (options.name) {
this.options.name = options.name;
} // End of IF
if (options.closable) {
this.options.closable = options.closable;
} // End of IF
return this;
};
// Prototypes
/**
* Displays some information concerning the current Modal object.
* #returns {Object} this Returns current modal object.
*/
Modal.prototype.open = function() {
let demo = document.getElementById('demo');
return this;
};
/**
* Creates an instance of a Modal object with the specified object elements.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.create = function() {
// Create Modal Element
let modal = document.createElement('div');
let modalBody = document.createElement('div');
// Create Modal
!modal.classList.contains('modal') ?
modal.classList.add('modal') :
modal.classList.add('');
modal.id = this.options.id || 'noID';
// Create modal body element
!modalBody.classList.contains('modal-body') ?
modalBody.classList.add('modal-body') :
modalBody.classList.add('');document.querySelector('#' + this.options.id + ' .modal-close');
modal.appendChild(modalBody);
// Adding modal sub-elements
if (this.options.title) {
let modalTitle = document.createElement('h2');
!modalTitle.classList.contains('modal-title') ?
modalTitle.classList.add('modal-title') :
modalTitle.classList.add('');
modalTitle.textContent = this.options.title;
modalBody.appendChild(modalTitle);
console.log('Added title!');
} // End of IF
if (this.options.subtitle) {
let modalSubtitle = document.createElement('h4');
!modalSubtitle.classList.contains('modal-subtitle') ?
modalSubtitle.classList.add('modal-subtitle') :
modalSubtitle.classList.add('');
modalSubtitle.textContent = this.options.subtitle;
modalBody.appendChild(modalSubtitle);
console.log('Added subtitle!');
} // End of IF
if (this.options.content) {
let modalContent = document.createElement('p');
!modalContent.classList.contains('modal-content') ?
modalContent.classList.add('modal-content') :
modalContent.classList.add('');
modalContent.textContent = this.options.content;
modalBody.appendChild(modalContent);
console.log('Added contents!');
} // End of IF
if (this.options.closable) {
let modalClose = document.createElement('span');
!modalClose.classList.contains('modal-close') ?
modalClose.classList.add('modal-close') :
modalClose.classList.add('');
modalClose.innerHTML = '×';
modalBody.appendChild(modalClose);
console.log('Close button added!');
} // End of IF
document.body.appendChild(modal);
console.log('Modal created with ID', modal.id);
return this;
};
/**
* Closes the current Modal object.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.close = function() {
let modal = document.getElementById(this.options.id);
let modalBody = modal.children[0];
// Delete elements from Modal Body
for (let i = 0; i < modalBody.children.length; i++) {
modalBody.removeChild(modalBody.children[i]);
} // End of LOOP
// Delete Modal Body from Modal
modal.removeChild(modalBody);
// Delete Modal from DOM
modal.style.display = 'none';
document.body.removeChild(modal);
return this;
};
/**
* Initializes the inner functions of the modal, such as the closing capacity.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.init = function(e) {
// let closeBtnAll = document.querySelectorAll('.modal-close');
let closeBtn = document.querySelector('#'+this.options.id + ' .modal-close');
// Assign close() function to all close buttons
closeBtn.addEventListener('click', () => {
if (this.options.closable) {
this.close();
}
})
// Press ESC to close ALL modals
return this;
};
// Create a Modal object
let modal1 = new Modal({
id: 'post1',
name: 'modal',
title: 'First Post',
subtitle: 'I contain all the elements',
content: 'This is awesome!',
closable: true
});
let modal2 = new Modal({
title: 'Second Post',
subtitle: 'Trying new things',
content: 'Hehehehehe',
closable: true
});
modal1.open();
modal1.create();
modal1.init();
modal2.open();
modal2.create();
modal2.init();
Just replace the above JS code in your codepen and try. It will work.
The problem is the init-function:
/**
* Initializes the inner functions of the modal, such as the closing capacity.
* #returns {Object} this Returns current Modal object.
*/
Modal.prototype.init = function() {
// let closeBtnAll = document.querySelectorAll('.modal-close');
let modal = document.getElementById(this.options.id);
let closeBtn = modal.querySelector('.modal-close');
// Assign close() function to all close buttons
closeBtn.addEventListener('click', () => {
console.log(this.options);
if (this.options.closable) {
this.close();
}
})
// Press ESC to close ALL modals
return this;
};
If you dont specify that you want to use the eventlistener on the current modal then it will be set to both modals.
I'm trying to implement a function which would calculate the servings for the ingredients from my website.
That function is in Recipe.js file and looks like that:
updateServings(type) {
// Servings
const newServings = type === 'dec' ? this.servings - 1 : this.servings + 1;
// Ingredients
this.ingredients.forEach((ingr) => {
ingr.count = this.capDecimal(ingr.count * (newServings / this.servings));
});
this.servings = newServings;
}
The problem is that when I console.log(state.recipe); in index.js this event Listener works, it will console log state.recipe after clicking - or + button on the website but it wont change the amount of serving in the recipe object:
elements.recipe.addEventListener('click', e => {
if(e.target.matches('.btn-decrease .btn-decrease *')){
//Decrease button is clicked
if(state.recipe.servings > 1){
state.recipe.updateServings('dec');
}
}else if(e.target.matches('.btn-increase .btn-increase *')){
//Increase button was clicked
state.recipe.updateServings('inc');
}
console.log(state.recipe);
});
I clicked 2 times but property serving still says 4 like here:
https://forum.toshitimes.com/uploads/toshitimes/original/2X/6/6bada9081879db1a14df9bad010382606fda253f.png
It a bigger project so I believe I need to include the whole repository from github: https://github.com/damianjnc/forkifyApp.git
What I need to change to make it work?
You need to update the view after the click event
elements.recipe.addEventListener('click', e => {
//....
try {
recipeView.clearRecipe();
recipeView.renderRecipe(state.recipe);
} catch (error) {
alert('error processing the recipe:(');
}
});
note: you need to declare your class properties
export default class Recipe {
ingredients;
servings;
constructor(id) {
this.id = id;
}
and you need map instead of forEach
this.ingredients = this.ingredients.map((ingr) => {
ingr.count = this.capDecimal(ingr.count * (newServings / this.servings));
return ingr;
});
I am using Shopify "Empire Theme" with quick product view and quick add to cart button and recently I added infinite scroll to products on each collection using Ajaxinate.js.
When I open a collection page it loads with some products which is supposed to do, The products already there work fine with quick view and quick add to cart and also.
The Infinite scroll works fine and it loads new product fine but the problem is raised when the new products loaded through AJAX call doesnt have quick add to cart and quick view function.
In short the product which are loaded with AJAX call dont have Quick View and add to cart function on other hand the product which are not loaded with AJAX and are already there have the above mentioned functionalities.
If you want to see the store here is the link. Check the first 8 products you will see they work fine, both the + icon (add item to cart) and the quick view is fine too. But after 8 items nothing works.
https://www.gyftss.myshopify.com
password: 1239
Code for Ajax infinite scroll is:
'use strict';
/* ===================================================================================== #preserve =
___ _ _ _
/ || | | | | |
\__ | | | | | | __
/ |/ |/_) |/ / \_/\/
\___/|__/| \_/|__/\__/ /\_/
|\
|/
Ajaxinate
version v2.0.6
https://github.com/Elkfox/Ajaxinate
Copyright (c) 2017 Elkfox Co Pty Ltd
https://elkfox.com
MIT License
================================================================================================= */
var Ajaxinate = function ajaxinateConstructor(config) {
var settings = config || {};
/*
pagination: Selector of pagination container
method: [options are 'scroll', 'click']
container: Selector of repeating content
offset: 0, offset the number of pixels before the bottom to start loading more on scroll
loadingText: 'Loading', The text changed during loading
callback: null, function to callback after a new page is loaded
*/
var defaultSettings = {
pagination: '.AjaxinatePagination',
method: 'scroll',
container: '.AjaxinateLoop',
offset: 660,
loadingText: '<img src="https://cdn.shopify.com/s/files/1/0066/5072/4415/files/spinner.gif?16236020128462925067" style="width:40px">',
callback: null
};
// Merge configs
this.settings = Object.assign(defaultSettings, settings);
// Bind 'this' to applicable prototype functions
this.addScrollListeners = this.addScrollListeners.bind(this);
this.addClickListener = this.addClickListener.bind(this);
this.checkIfPaginationInView = this.checkIfPaginationInView.bind(this);
this.stopMultipleClicks = this.stopMultipleClicks.bind(this);
this.destroy = this.destroy.bind(this);
// Set up our element selectors
this.containerElement = document.querySelector(this.settings.container);
this.paginationElement = document.querySelector(this.settings.pagination);
this.initialize();
};
Ajaxinate.prototype.initialize = function initializeTheCorrectFunctionsBasedOnTheMethod() {
// Find and initialise the correct function based on the method set in the config
if (this.containerElement) {
var initializers = {
click: this.addClickListener,
scroll: this.addScrollListeners
};
initializers[this.settings.method]();
}
};
Ajaxinate.prototype.addScrollListeners = function addEventListenersForScrolling() {
if (this.paginationElement) {
document.addEventListener('scroll', this.checkIfPaginationInView);
window.addEventListener('resize', this.checkIfPaginationInView);
window.addEventListener('orientationchange', this.checkIfPaginationInView);
}
};
Ajaxinate.prototype.addClickListener = function addEventListenerForClicking() {
if (this.paginationElement) {
this.nextPageLinkElement = this.paginationElement.querySelector('a');
this.clickActive = true;
if (typeof this.nextPageLinkElement !== 'undefined') {
this.nextPageLinkElement.addEventListener('click', this.stopMultipleClicks);
}
}
};
Ajaxinate.prototype.stopMultipleClicks = function handleClickEvent(event) {
event.preventDefault();
if (this.clickActive) {
this.nextPageLinkElement.innerHTML = this.settings.loadingText;
this.nextPageUrl = this.nextPageLinkElement.href;
this.clickActive = false;
this.loadMore();
}
};
Ajaxinate.prototype.checkIfPaginationInView = function handleScrollEvent() {
var top = this.paginationElement.getBoundingClientRect().top - this.settings.offset;
var bottom = this.paginationElement.getBoundingClientRect().bottom + this.settings.offset;
if (top <= window.innerHeight && bottom >= 0) {
this.nextPageLinkElement = this.paginationElement.querySelector('a');
this.removeScrollListener();
if (this.nextPageLinkElement) {
this.nextPageLinkElement.innerHTML = this.settings.loadingText;
this.nextPageUrl = this.nextPageLinkElement.href;
this.loadMore();
}
}
};
Ajaxinate.prototype.loadMore = function getTheHtmlOfTheNextPageWithAnAjaxRequest() {
this.request = new XMLHttpRequest();
this.request.onreadystatechange = function success() {
if (this.request.readyState === 4 && this.request.status === 200) {
var newContainer = this.request.responseXML.querySelectorAll(this.settings.container)[0];
var newPagination = this.request.responseXML.querySelectorAll(this.settings.pagination)[0];
this.containerElement.insertAdjacentHTML('beforeend', newContainer.innerHTML);
this.paginationElement.innerHTML = newPagination.innerHTML;
if (this.settings.callback && typeof this.settings.callback === 'function') {
this.settings.callback(this.request.responseXML);
}
this.initialize();
}
}.bind(this);
this.request.open('GET', this.nextPageUrl);
this.request.responseType = 'document';
this.request.send();
};
Ajaxinate.prototype.removeClickListener = function removeClickEventListener() {
this.nextPageLinkElement.addEventListener('click', this.stopMultipleClicks);
};
Ajaxinate.prototype.removeScrollListener = function removeScrollEventListener() {
document.removeEventListener('scroll', this.checkIfPaginationInView);
window.removeEventListener('resize', this.checkIfPaginationInView);
window.removeEventListener('orientationchange', this.checkIfPaginationInView);
};
Ajaxinate.prototype.destroy = function removeEventListenersAndReturnThis() {
// This method is used to unbind event listeners from the DOM
// This function is called manually to destroy "this" Ajaxinate instance
var destroyers = {
click: this.removeClickListener,
scroll: this.removeScrollListener
};
destroyers[this.settings.method]();
return this;
};
The initialization Code is:
<script>
document.addEventListener("DOMContentLoaded", function(e) {
e.preventDefault();
var endlessScroll = new Ajaxinate();
offset: 660
});
</script>
The theme javascript file for theme can be found here:
https://cdn.shopify.com/s/files/1/0066/5072/4415/t/5/assets/empire.js?11964077371852938126
Thanks in advance.
With this code I was able to solve the quick look of the theme empire
<script>
var empire_js_o = '<script src="//cdn.shopify.com/s/files/1/1540/2631/t/34/assets/empire.js?9521252849869083980" data-scripts="" data-shopify-api-url="//cdn.shopify.com/s/assets/themes_support/api.jquery-0ea851da22ae87c0290f4eeb24bc8b513ca182f3eb721d147c009ae0f5ce14f9.js" data-shopify-currencies="//cdn.shopify.com/s/javascripts/currencies.js" data-shopify-countries="/services/javascripts/countries.js" data-shopify-common="//cdn.shopify.com/s/assets/themes_support/shopify_common-040322ee69221c50a47032355f2f7e6cbae505567e2157d53dfb0a2e7701839c.js" data-shopify-cart="//cdn.shopify.com/s/files/1/1540/2631/t/34/assets/jquery.cart.js?9521252849869083980" id="empire_js">';
var NewEmpire = function reload_js() {
$('#empire_js').remove();
$(empire_js_o).appendTo('head');
}
document.addEventListener("DOMContentLoaded", function() {
var endlessScroll = new Ajaxinate({
method: 'scroll',
loadingText: 'Loading...',
callback: NewEmpire
});
});
</script>
I am trying to develop a combobox that allows the user to enter value which get lookuped and if it is missing in the list it get viualize as a new value (I'd thought about a image left to the field) and in additon causes the field to be bound to another property of the model where it is assigned to.
In addition the user should be able to clear the combo value.
I started by using a provided class from this guy and extended it with the required settings. I managed to prevent a issue which causes the 'x' trigger to hide continious, but I couldn't manage the difference between userinput - selection - set - clear (set value by form)
Here is a snippet from what I've got so far
Ext.define('Ext.ux.form.field.InputCombo', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.inputcombo',
trigger2Cls: 'x-form-clear-trigger',
newRecordFieldname: null, // the name if this value is new
originName: null,
initComponent: function () {
var me = this;
me.addEvents(
/**
* #event beforeclear
* #param {Combo} Combo The combo that triggered the event
*/
'beforeclear',
/**
* #event beforeclear
* #param {Combo} Combo The combo that triggered the event
*/
'clear'
);
me.callParent(arguments);
me.on('specialkey', this.onSpecialKeyDown, me);
me.on('afterrender', function () {
me.onShowClearTrigger(false);
}, me);
this.on('change', function() {
this.createdRecord(this);
});
this.on('select', function() {
this.onSetExisting(this);
});
},
createdRecord: function(ref) {
ref.newRecord = true;
ref.selectedRecord = false;
ref.originName = ref.name;
ref.name = ref.newRecordFieldname;
ref.style = 'background:url(../resources/themes/images/default/dd/drop-add.gif) no-repeat left center;'; // does not work
},
setExisting: function(ref) {
ref.newRecord = false;
ref.selectedRecord = true;
ref.style = '';
ref.name = ref.originName;
},
/**
* #private onSpecialKeyDown
* eventhandler for special keys
*/
onSpecialKeyDown: function (obj, e, opt) {
if ( e.getKey() == e.ESC ) {
this.clear();
}
},
onShowClearTrigger: function (show) {
var me = this;
if (show) {
me.triggerEl.each(function (el, c, i) {
if (i === 1) {
el.setWidth(el.originWidth, false);
el.setVisible(true);
me.active = true;
}
});
} else {
me.triggerEl.each(function (el, c, i) {
if (i === 1) {
el.originWidth = el.getWidth();
if (el.originWidth !== 0) { // prevent double hide
el.setWidth(0, false);
el.setVisible(false);
me.active = false;
}
}
});
}
me.updateLayout();
},
/* lines remoced. see linked answer */
});
What I haven't get managed to get working (and the point where I need help) is to show a image for a new record and that the field behaves in the same manner regardless if I first enter something and clear or set a value, select a value and so on. By know the field behaves quiete different or does not work at all.
In additon it should be compatible to the upcoming 4.2 release. Dunno if this important to mention.
Any help appreciated!