use events after loading script - javascript

I have two events which use functions from script. How to start use this functions only after loading script?
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/intl-tel-input/js/intlTelInput.js"></script>
<script>
var settings = {
utilsScript: "../lib/intl-tel-input/js/utils.js",
initialCountry: "auto",
geoIpLookup: function (callback) {
$.get("http://ipinfo.io", function () { }, "jsonp").always(function (resp) {
var countryCode = (resp && resp.country) ? resp.country : "";
callback(countryCode);
});
}
};
$("#phoneNumber").intlTelInput(settings);
$("#viber").intlTelInput(settings);
$("#phoneNumber").on('input', function (e) {
var intlNumber = $("#phoneNumber").intlTelInput("getNumber");
$("#viber").intlTelInput("setNumber", intlNumber);
});
$("#phoneNumber").on("countrychange", function (e, countryData) {
$("#viber").intlTelInput("setCountry", countryData.iso2);
});
</script>
in google chrome console:
Uncaught TypeError: $(...).intlTelInput is not a function

use $(document).ready(function(){})
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/intl-tel-input/js/intlTelInput.js"></script>
<script>
$(document).ready(function(){
var settings = {
utilsScript: "../lib/intl-tel-input/js/utils.js",
initialCountry: "auto",
geoIpLookup: function (callback) {
$.get("http://ipinfo.io", function () { }, "jsonp").always(function (resp) {
var countryCode = (resp && resp.country) ? resp.country : "";
callback(countryCode);
});
}
};
$("#phoneNumber").intlTelInput(settings);
$("#viber").intlTelInput(settings);
$("#phoneNumber").on('input', function (e) {
var intlNumber = $("#phoneNumber").intlTelInput("getNumber");
$("#viber").intlTelInput("setNumber", intlNumber);
});
$("#phoneNumber").on("countrychange", function (e, countryData) {
$("#viber").intlTelInput("setCountry", countryData.iso2);
});
});
</script>

I'm located script in Scripts section and it solve the problem.
#section Scripts {
<script src="~/lib/intl-tel-input/js/intlTelInput.js"></script>
<script>
$(document).ready(function () {
var settings = {
utilsScript: "../lib/intl-tel-input/js/utils.js",
initialCountry: "auto",
geoIpLookup: function (callback) {
$.get("http://ipinfo.io", function () { }, "jsonp").always(function (resp) {
var countryCode = (resp && resp.country) ? resp.country : "";
callback(countryCode);
});
}
};
$("#phoneNumber").intlTelInput(settings);
$("#viber").intlTelInput(settings);
$("#phoneNumber").on('input', function (e) {
var intlNumber = $("#phoneNumber").intlTelInput("getNumber");
$("#viber").intlTelInput("setNumber", intlNumber);
});
$("#phoneNumber").on("countrychange", function (e, countryData) {
$("#viber").intlTelInput("setCountry", countryData.iso2);
});
});
</script>

Try to include "intlTelInput-jquery.min.js" from the library under jquery. It solved my problem.
<script src="//cdnjs.cloudflare.com/ajax/libs/intl-tel-input/14.0.6/js/intlTelInput-jquery.min.js"></script>

Related

To-Do List, and at 5 elements checked to show a button

How can I make a To-Do List, and at 5 elements checked to show a button?
I have already done the To-Do list (in HTML and JavaScript - my code below ), but I don't know how can I write a code to show a button at 5 elements checked.
My JSFiddle:
https://jsfiddle.net/sd355qxp
My code (in HTML and JavaScript) :
<html>
<head>
<link rel="stylesheet" href="tomo.css">
<title>TOMO</title>
</head>
<body>
<h1>TOMO</h1>
<center>
<div id="todo-app">
<label class="todo-label" for="new-todo">What do you have to do today?</label>
<input type="text" id="new-todo" class="todo-input" placeholder="english homework">
<ul id="todo-list" class="count-this"></ul>
<div id="todo-stats"></div>
</div>
</center>
<script type="text/x-template" id="todo-item-template">
<div class="todo-view">
<input type="checkbox" class="todo-checkbox" {checked}>
<span class="todo-content" tabindex="0">{text}</span>
</div>
<div class="todo-edit">
<input type="text" class="todo-input" value="{text}">
</div>
<a href="#" class="todo-remove" title="Remove this task">
<span class="todo-remove-icon"></span>
</a>
</script>
<script type="text/x-template" id="todo-stats-template">
<span class="todo-count">
<span class="todo-remaining">{numRemaining}</span>
<span class="todo-remaining-label">{remainingLabel}</span> left.
</span>
<a href="#" class="todo-clear">
Clear <span class="todo-done">{numDone}</span>
completed <span class="todo-done-label">{doneLabel}</span>
</a>
</script>
<script src="http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js"></script>
<script>
YUI().use('event-focus', 'json', 'model', 'model-list', 'view', function (Y) {
var TodoAppView, TodoList, TodoModel, TodoView;
TodoModel = Y.TodoModel = Y.Base.create('todoModel', Y.Model, [], {
sync: LocalStorageSync('todo'),
toggleDone: function () {
this.set('done', !this.get('done')).save();
}
}, {
ATTRS: {
done: {value: false},
text: {value: ''}
}
});
TodoList = Y.TodoList = Y.Base.create('todoList', Y.ModelList, [], {
model: TodoModel,
sync: LocalStorageSync('todo'),
done: function () {
return this.filter(function (model) {
return model.get('done');
});
},
remaining: function () {
return this.filter(function (model) {
return !model.get('done');
});
}
});
TodoAppView = Y.TodoAppView = Y.Base.create('todoAppView', Y.View, [], {
events: {
'#new-todo': {keypress: 'createTodo'},
'.todo-clear': {click: 'clearDone'},
'.todo-item': {
mouseover: 'hoverOn',
mouseout : 'hoverOff'
}
},
template: Y.one('#todo-stats-template').getHTML(),
initializer: function () {
var list = this.todoList = new TodoList();
list.after('add', this.add, this);
list.after('reset', this.reset, this);
list.after(['add', 'reset', 'remove', 'todoModel:doneChange'],
this.render, this);
list.load();
},
render: function () {
var todoList = this.todoList,
stats = this.get('container').one('#todo-stats'),
numRemaining, numDone;
if (todoList.isEmpty()) {
stats.empty();
return this;
}
numDone = todoList.done().length;
numRemaining = todoList.remaining().length;
stats.setHTML(Y.Lang.sub(this.template, {
numDone : numDone,
numRemaining : numRemaining,
doneLabel : numDone === 1 ? 'task' : 'tasks',
remainingLabel: numRemaining === 1 ? 'task' : 'tasks'
}));
if (!numDone) {
stats.one('.todo-clear').remove();
}
return this;
},
add: function (e) {
var view = new TodoView({model: e.model});
this.get('container').one('#todo-list').append(
view.render().get('container')
);
},
clearDone: function (e) {
var done = this.todoList.done();
e.preventDefault();
this.todoList.remove(done, {silent: true});
Y.Array.each(done, function (todo) {
todo.destroy({remove: true});
});
this.render();
},
createTodo: function (e) {
var inputNode, value;
if (e.keyCode === 13) { // enter key
inputNode = this.get('inputNode');
value = Y.Lang.trim(inputNode.get('value'));
if (!value) { return; }
this.todoList.create({text: value});
inputNode.set('value', '');
}
},
hoverOff: function (e) {
e.currentTarget.removeClass('todo-hover');
},
hoverOn: function (e) {
e.currentTarget.addClass('todo-hover');
},
reset: function (e) {
var fragment = Y.one(Y.config.doc.createDocumentFragment());
Y.Array.each(e.models, function (model) {
var view = new TodoView({model: model});
fragment.append(view.render().get('container'));
});
this.get('container').one('#todo-list').setHTML(fragment);
}
}, {
ATTRS: {
container: {
valueFn: function () {
return '#todo-app';
}
},
inputNode: {
valueFn: function () {
return Y.one('#new-todo');
}
}
}
});
TodoView = Y.TodoView = Y.Base.create('todoView', Y.View, [], {
containerTemplate: '<li class="todo-item"/>',
events: {
'.todo-checkbox': {click: 'toggleDone'},
'.todo-content': {
click: 'edit',
focus: 'edit'
},
'.todo-input' : {
blur : 'save',
keypress: 'enter'
},
'.todo-remove': {click: 'remove'}
},
template: Y.one('#todo-item-template').getHTML(),
initializer: function () {
var model = this.get('model');
model.after('change', this.render, this);
model.after('destroy', function () {
this.destroy({remove: true});
}, this);
},
render: function () {
var container = this.get('container'),
model = this.get('model'),
done = model.get('done');
container.setHTML(Y.Lang.sub(this.template, {
checked: done ? 'checked' : '',
text : model.getAsHTML('text')
}));
container[done ? 'addClass' : 'removeClass']('todo-done');
this.set('inputNode', container.one('.todo-input'));
return this;
},
edit: function () {
this.get('container').addClass('editing');
this.get('inputNode').focus();
},
enter: function (e) {
if (e.keyCode === 13) {
Y.one('#new-todo').focus();
}
},
remove: function (e) {
e.preventDefault();
this.constructor.superclass.remove.call(this);
this.get('model').destroy({'delete': true});
},
save: function () {
this.get('container').removeClass('editing');
this.get('model').set('text', this.get('inputNode').get('value')).save();
},
toggleDone: function () {
this.get('model').toggleDone();
}
});
function LocalStorageSync(key) {
var localStorage;
if (!key) {
Y.error('No storage key specified.');
}
if (Y.config.win.localStorage) {
localStorage = Y.config.win.localStorage;
}
var data = Y.JSON.parse((localStorage && localStorage.getItem(key)) || '{}');
function destroy(id) {
var modelHash;
if ((modelHash = data[id])) {
delete data[id];
save();
}
return modelHash;
}
function generateId() {
var id = '',
i = 4;
while (i--) {
id += (((1 + Math.random()) * 0x10000) | 0)
.toString(16).substring(1);
}
return id;
}
function get(id) {
return id ? data[id] : Y.Object.values(data);
}
function save() {
localStorage && localStorage.setItem(key, Y.JSON.stringify(data));
}
function set(model) {
var hash = model.toJSON(),
idAttribute = model.idAttribute;
if (!Y.Lang.isValue(hash[idAttribute])) {
hash[idAttribute] = generateId();
}
data[hash[idAttribute]] = hash;
save();
return hash;
}
return function (action, options, callback) {
var isModel = Y.Model && this instanceof Y.Model;
switch (action) {
case 'create': // intentional fallthru
case 'update':
callback(null, set(this));
return;
case 'read':
callback(null, get(isModel && this.get('id')));
return;
case 'delete':
callback(null, destroy(isModel && this.get('id')));
return;
}
};
}
new TodoAppView();
});
</script>
</body>
</html>
Can you use jQuery?
$(".todo-checkbox").change(function(){
if($(".todo-checkbox:checked").length > 4){
$("#yourButton").show();
}
});

Select2 4 custom data adapter

I am trying to create a custom data adapter according to an example here: http://select2.github.io/announcements-4.0.html#query-to-data-adapter.
How can I move the line that creates the select2 control outside the function with definition of DataAdapter (see the code below)?
<!DOCTYPE html>
<head>
<title></title>
<link href="select2.css" rel="stylesheet" />
<script type="text/javascript" src="http://code.jquery.com/jquery-2.1.4.js"></script>
<script type="text/javascript" src="select2.full.js"></script>
<script type="text/javascript">
$.fn.select2.amd.require(
['select2/data/array', 'select2/utils'],
function (ArrayData, Utils) {
function CustomData ($element, options) {
CustomData.__super__.constructor.call(this, $element, options);
}
Utils.Extend(CustomData, ArrayData);
CustomData.prototype.query = function (params, callback) {
var data = {results: []};
data.results.push({id: params.term, text: params.term});
data.results.push({id: 11, text: 'aa'});
data.results.push({id: 22, text: 'bb'});
callback(data);
};
// Works if uncommented, but this line needs to be elsewhere (in $(document).ready()).
//$("#my").select2({tags: true, dataAdapter: CustomData});
});
$(document).ready(function() {
// This line does not work here.
$("#my").select2({tags: true, dataAdapter: CustomData});
});
</script>
</head>
<body>
<select id="my"></select>
</body>
</html>
you define it via AMD-Pattern:
$.fn.select2.amd.define('select2/data/customAdapter',[
'select2/data/array',
'select2/utils'
],
function (ArrayAdapter, Utils) {
function CustomDataAdapter ($element, options) {
CustomDataAdapter.__super__.constructor.call(this, $element, options);
}
Utils.Extend(CustomDataAdapter, ArrayAdapter);
CustomDataAdapter.prototype.current = function (callback) {
callback(...);
};
return CustomDataAdapter;
}
);
var customAdapter=$.fn.select2.amd.require('select2/data/customAdapter');
$("#my").select2({
tags: true,
dataAdapter: customAdapter
});
For anyone trying to extend select2, here is an example :
// Require the adapter you want to override
$.fn.select2.amd.require(["select2/data/select"], function (Select) {
let CustomDataAdapter = Select;
// Simple example, just override the function
CustomDataAdapter.prototype.current = function (callback) {
// Your own code
};
// Example modifying data then calling the original function (which we need to keep)
let originalSelect = CustomDataAdapter.prototype.select;
CustomDataAdapter.prototype.select = function (data) {
// Your own code
// Call the original function while keeping 'this' context
originalSelect.bind(this)(data);
};
// Finally, use the custom data adapter
$('#my-select').select2({
dataAdapter: CustomDataAdapter
});
});
Example of select2 to handle big array. I am fetching data from server using ajax. Handling searching and pagination locally with more than 20000 data json.
<select id="widget_project"></select>
<script>
$(function () {
allProjects;// having all project json data
pageSize = 50
jQuery.fn.select2.amd.require(["select2/data/array", "select2/utils"],
function (ArrayData, Utils) {
function CustomData($element, options) {
CustomData.__super__.constructor.call(this, $element, options);
}
Utils.Extend(CustomData, ArrayData);
CustomData.prototype.query = function (params, callback) {
var results = [];
if(p_term !== "" && typeof params.term === "undefined"){
params.term = p_term;
console.log(params.term);
}
if (params.term && params.term !== '') {
p_term = params.term;
results = findItem(params.term);
} else {
results = allProjects;
}
if (!("page" in params)) {
params.page = 1;
}
var data = {};
data.results = results.slice((params.page - 1) * pageSize, params.page * pageSize);
data.pagination = {};
data.pagination.more = params.page * pageSize < results.length;
callback(data);
};
$(document).ready(function () {
$("#widget_project").select2({
minimumInputLength: 3,
placeholder:"Select a project",
ajax: {},
dataAdapter: CustomData
});
});
})
});
</script>

using two fancybox methods together

So I have the code for fancybox:
$(".fancybox").fancybox({
beforeShow: function () {
var alt = this.element.find('img').attr('alt');
this.inner.find('img').attr('alt', alt);
this.title = alt;
}
});
and
$(document).ready(function () {
$(".fancybox").fancybox({
helpers: {
overlay: {
locked: false
}
}
});
});
when I try to use them both I get a syntax error:
$(document).ready(function () {
$(".fancybox").fancybox({
helpers: {
overlay: {
locked: false
}
}
beforeShow: function () {
var alt = this.element.find('img').attr('alt');
this.inner.find('img').attr('alt', alt);
this.title = alt;
}
});
});
and when I use them separately neither of the fancybox functions work.
<script>
$(document).ready(function () {
$(".fancybox").fancybox({
helpers: {
overlay: {
locked: false
}
}
});
});
</script>
<script>
$(".fancybox").fancybox({
beforeShow: function () {
var alt = this.element.find('img').attr('alt');
this.inner.find('img').attr('alt', alt);
this.title = alt;
}
});
</script>
How can I use them both?
You're missing a comma in your object properties on line 7 between helpers: { ... } and beforeShow: function() { ... }
$(document).ready(function () {
$(".fancybox").fancybox({
helpers: {
overlay: {
locked: false
}
}, // <-- this comma right here is missing in your code
beforeShow: function () {
var alt = this.element.find('img').attr('alt');
this.inner.find('img').attr('alt', alt);
this.title = alt;
}
});
});

How to call a script on success of $.ajax

The goal
Call a script (to be more specific, qTip2 script) on success of $.ajax function from jQuery.
The problem
I have the following script to work with KnockoutJS:
$.ajax({
url: "/ProductsSummary/List?output=json",
dataType: "json",
success: function (data) {
var mappingOptions = {
create: function (options) {
return (new (function () {
this.finalMeasure = ko.computed(function () {
return this.quantity() > 1 ? this.measure() + "s"
: this.measure();
}, this);
this.infoComposition = ko.computed(function () {
return this.quantity() + ' ' + this.finalMeasure();
}, this);
ko.mapping.fromJS(options.data, {}, this);
})());
}
};
ViewModel.Summary.products = ko.mapping.fromJS(data, mappingOptions);
ko.applyBindings(ViewModel);
$("ul.products-list li").each(function () {
var $productId = $(this).data("productid"),
$match = $(".summary")
.find("li[data-productid=" + $productId + "]")
.length > 0;
if ($match) {
$(this).find("button.action").addClass("remove");
} else {
$(this).find("button.action").addClass("add");
}
});
}
});
In the following fragment, I set a class to a button to define its style, but I have no success because I need to call the qTip2 again to render the new tooltip. See:
$("ul.products-list li").each(function () {
var $productId = $(this).data("productid"),
$match = $(".summary")
.find("li[data-productid=" + $productId + "]")
.length > 0;
if ($match) {
$(this).find("button.action").addClass("remove");
} else {
$(this).find("button.action").addClass("add");
}
});
To make the things work, I have to call the qTip2 script after this fragment, but it was previously called.
To do not be redundant or practice the DRY concept, how can I call, inside of the "on success", qTip2 script again?
A little of code
My qTip2 Script:
$(function () {
var targets = $("ul.products-list li .controls
button.action.add").attr('oldtitle', function () {
var title = $(this).attr('title');
return $(this).removeAttr('title'), title;
}),
shared_config = {
content: {
text: function (event, api) {
return $(event.currentTarget).attr('oldtitle');
}
},
position: {
viewport: $(window),
my: "bottom center",
at: "top center",
target: "event"
},
show: {
target: targets
},
hide: {
target: targets
},
style: "qtip-vincae qtip-vincae-green"
};
$('<div/>').qtip(shared_config);
$('<div/>').qtip($.extend(true, shared_config, {
content: {
text: function (event, api) {
var target = $(event.currentTarget),
content = target.data("tooltipcontent");
if (!content) {
content = target.next("div:hidden");
content && target.data("tooltipcontent", content);
}
return content || false;
}
},
position: {
my: "top center",
at: "bottom center",
viewport: $(".content"),
container: $(".content")
},
show: {
event: "click",
effect: function () {
$(this).show(0, function () {
$(this).find("input[name=productQuantity]").focus();
});
}
},
hide: {
event: "click unfocus",
fixed: false
},
events: {
hide: function (event, api) {
$($(this).find("input[type=text].quantity")).val("");
$($(this).find("button")).prop("disabled", true);
}
},
style: "qtip-vincae qtip-vincae-grey"
}));
});
Thanks in advance.
As I see it you should call simply $(this).find("button.action").qtip() or .qtip(...) and set/update what you want.
This demo could be your cheat-sheet:
http://craigsworks.com/projects/qtip2/demos/#validation

Getting a json from collection to view in backbone.js

I am new to backbone.js and is having some problem with my app. I have a collection which relies on a json data source. I am able to console.log the json in my parse method. Is that enough to bind the collection to my view? I don't understand the use of fetch method.
My collection looks like as follows,
(function (collections,model) {
collections.Directory = Backbone.Collection.extend({
initialize : function(){
console.log('we are here');
},
model:model.item,
url:'collections/json/data.json',
parse:function(response){
console.log(response);
return response;
}
});
})(app.collections,app.models);
And my master view looks like this,
(function(views,collections){
views.masterView = Backbone.View.extend({
el : $("#contacts"),
initialize : function(){
console.log('view initialize inside render');
this.render();
this.$el.find("#filter").append(this.createSelect());
this.on("change:filterType", this.filterByType, this);
this.collection.on("reset", this.render, this);
this.collection.on("add", this.renderContact, this);
//console.log('we are here'+app.collections.CollectionItems.fetch());
console.log('view initialize');
},
render : function(){
this.$el.find("article").remove();
_.each(this.collection.models,function(item){
this.renderContact(item);
},this);
},
renderContact: function (item) {
views.contactView = new app.views.ContactView({
model: item
});
this.$el.append(contactView.render().el);
},
getTypes : function () {
return _.uniq(this.collection.pluck("Qno"));
},
createSelect : function () {
var select = $("<select/>", {
html: "<option value='all'>All</option>"
});
_.each(this.getTypes(), function (item) {
var option = $("<option/>", {
value: item.toLowerCase(),
text: item.toLowerCase()
}).appendTo(select);
});
return select;
},
events: {
"change #filter select": "setFilter",
"click #add": "addContact",
"click #showForm": "showForm"
},
setFilter : function(e){
this.filterType = e.currentTarget.value;
this.trigger("change:filterType");
},
filterByType: function () {
if (this.filterType === "all") {
this.collection.reset(contacts);
routerURL.navigate("filter/all");
} else {
this.collection.reset(contacts, { silent: true });
var filterType = this.filterType,
filtered = _.filter(this.collection.models, function (item) {
return item.get("type").toLowerCase() === filterType;
});
this.collection.reset(filtered);
routerURL.navigate("filter/"+filterType);
}
},
addContact : function(e){
e.preventDefault();
var contModel = {};
$("#addContact").children("input").each(function(i, el){
if($(el).val() !== "")
contModel[el.id] = $(el).val();
});
contacts.push(contModel);
if (_.indexOf(this.getTypes(), contModel.type) === -1) {
this.collection.add(new Contact(contModel));
this.$el.find("#filter").find("select").remove().end().append(this.createSelect());
} else {
this.collection.add(new Contact(contModel));
}
},
showForm : function(){
this.$el.find("#addContact").slideToggle();
}
});
})(app.views,app.collections);
my model is very simple and looks like this,
(function ( models ) {
models.Item = Backbone.Model.extend({
defaults :{Qno:'1',Desc:'hello'}
});
})( app.models );
ihave one js file instantiatin viewsand collections
(function () {
window.app = {};
app.collections = {};
app.models = {};
app.views = {};
app.mixins = {};
$(function(){
app.collections.CollectionItems = new app.collections.Directory();
//app.collections.CollectionItems.fetch();
//console.log(app.collections.CollectionItems.fetch());
app.collections.CollectionItems.fetch({
success: function (collection,response) {
console.log(response);
}
});
//console.log(app.collections.CollectionItems.toJSON());
console.log('coll started');
app.views.app = new app.views.masterView({collection: app.collections.CollectionItems});
console.log('view is jus about fine!!');
//app.views.pagination = new app.views.PaginatedView({collection:app.collections.paginatedItems});
});
var ContactsRouter = Backbone.Router.extend({
routes: {
"filter/:type": "urlFilter"
},
urlFilter: function (type) {
master.filterType = type;
master.trigger("change:filterType");
}
});
var routerURL = new ContactsRouter();
Backbone.history.start();
})();
my landing page looks like this with a template in it
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Backbone.js Web App</title>
<link rel="stylesheet" href="css/screen.css" />
</head>
<body>
<div id="contacts">
</div>
<script id="contactTemplate" type="text/template">
<h1><%= Qno %></h1>
</script>
<script src="js/jquery.js"></script>
<script src="js/underscore-min.js"></script>
<script src="js/backbone-min.js"></script>
<script src="app.js"></script>
<script src="collections/Directory.js"></script>
<script src="models/item.js"></script>
<script src="views/masterView.js"></script>
<script src="views/simpleView.js"></script>
<!--<script src="js/backbone.paginator.js"></script>-->
</body>
</html>
I just can't get my head around this. The view is not rendered with the collection data. Please help!
I think it's because the fetch method on your collection is executed asynchronously and has therefore not completed when you create your view (if you look at the console I would expect the log statement in the success callback to display after the log statements underneath). This means that your view render method is called before the collection is populated and the reset event (which you're binding to in your view) is never triggered.
Try updating the code which instantiates everything as follows:
$(function(){
app.collections.CollectionItems = new app.collections.Directory();
//app.collections.CollectionItems.fetch();
//console.log(app.collections.CollectionItems.fetch());
app.collections.CollectionItems.fetch({
success: function (collection,response) {
console.log(response);
app.views.app = new app.views.masterView({collection: app.collections.CollectionItems});
}
});
});

Categories

Resources