Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
72
gaia/apps/search/js/providers/bookmarks.js
Executable file
72
gaia/apps/search/js/providers/bookmarks.js
Executable file
|
|
@ -0,0 +1,72 @@
|
|||
/* globals DataGridProvider, GaiaGrid, Search,
|
||||
Promise, SyncDataStore, InMemoryStore */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var STORE_NAME = 'bookmarks_store';
|
||||
|
||||
function formatBookmark(bookmark) {
|
||||
return {
|
||||
data: new GaiaGrid.Bookmark({
|
||||
id: bookmark.url,
|
||||
name: bookmark.name || bookmark.url,
|
||||
url: bookmark.url,
|
||||
icon: bookmark.icon
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function Bookmarks() {}
|
||||
|
||||
Bookmarks.prototype = {
|
||||
|
||||
__proto__: DataGridProvider.prototype,
|
||||
|
||||
name: 'Bookmarks',
|
||||
|
||||
init: function() {
|
||||
DataGridProvider.prototype.init.apply(this, arguments);
|
||||
this.persistStore = new InMemoryStore();
|
||||
this.syncStore = new SyncDataStore(STORE_NAME, this.persistStore, 'url');
|
||||
return this.syncStore.sync();
|
||||
},
|
||||
|
||||
get: function(url) {
|
||||
var results = this.persistStore.results;
|
||||
|
||||
for (var idx in results) {
|
||||
var item = results[idx];
|
||||
if (item.url === url) {
|
||||
return formatBookmark(item);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
search: function(filter) {
|
||||
var self = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
var renderResults = [];
|
||||
filter = filter.toLowerCase();
|
||||
for(var elem in self.persistStore.results) {
|
||||
var bookmark = self.persistStore.results[elem];
|
||||
var name = ('name' in bookmark) && bookmark.name.toLowerCase();
|
||||
if ((!bookmark.url || bookmark.url.indexOf(filter) == -1) &&
|
||||
(!name || name.indexOf(filter) == -1)) {
|
||||
continue;
|
||||
}
|
||||
renderResults.push(formatBookmark(bookmark));
|
||||
}
|
||||
resolve(renderResults);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
exports.Bookmarks = new Bookmarks();
|
||||
Search.provider(exports.Bookmarks);
|
||||
|
||||
}(window));
|
||||
77
gaia/apps/search/js/providers/contacts.js
Executable file
77
gaia/apps/search/js/providers/contacts.js
Executable file
|
|
@ -0,0 +1,77 @@
|
|||
(function() {
|
||||
|
||||
'use strict';
|
||||
/* global MozActivity, Promise, Provider, Search */
|
||||
|
||||
function Contacts() {
|
||||
}
|
||||
|
||||
Contacts.prototype = {
|
||||
|
||||
__proto__: Provider.prototype,
|
||||
|
||||
name: 'Contacts',
|
||||
|
||||
click: function(e) {
|
||||
var target = e.target;
|
||||
|
||||
Search.close();
|
||||
/* jshint nonew: false */
|
||||
new MozActivity({
|
||||
name: 'open',
|
||||
data: {
|
||||
type: 'webcontacts/contact',
|
||||
params: {
|
||||
'id': target.dataset.contactId
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
search: function(input) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var options = {
|
||||
filterValue: input,
|
||||
filterBy: ['givenName'],
|
||||
filterOp: 'startsWith'
|
||||
};
|
||||
|
||||
this.clear();
|
||||
|
||||
var request = navigator.mozContacts.find(options);
|
||||
request.onsuccess = (function() {
|
||||
var results = request.result;
|
||||
if (!results.length) {
|
||||
return reject();
|
||||
}
|
||||
|
||||
var renderResults = [];
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var result = results[i];
|
||||
for (var j = 0; j < result.name.length; j++) {
|
||||
var renderObj = {
|
||||
title: result.name[j],
|
||||
dataset: {
|
||||
contactId: result.id
|
||||
}
|
||||
};
|
||||
|
||||
if (result.photo) {
|
||||
renderObj.icon = result.photo[0];
|
||||
}
|
||||
|
||||
renderResults.push(renderObj);
|
||||
}
|
||||
}
|
||||
resolve(renderResults);
|
||||
}).bind(this);
|
||||
|
||||
request.onerror = function() {
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Search.provider(new Contacts());
|
||||
|
||||
}());
|
||||
33
gaia/apps/search/js/providers/grid_provider.js
Executable file
33
gaia/apps/search/js/providers/grid_provider.js
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
/* global Provider */
|
||||
|
||||
'use strict';
|
||||
|
||||
function DataGridProvider() {
|
||||
}
|
||||
|
||||
DataGridProvider.prototype = {
|
||||
|
||||
__proto__: Provider.prototype,
|
||||
|
||||
name: 'DataGridProvider',
|
||||
|
||||
isGridProvider: true,
|
||||
|
||||
init: function() {
|
||||
this.grid = document.getElementById('icons');
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
this.grid.clear();
|
||||
},
|
||||
|
||||
render: function(results) {
|
||||
results.forEach(function(config, index) {
|
||||
this.grid.add(config.data);
|
||||
}, this);
|
||||
|
||||
this.grid.render({
|
||||
skipDivider: true
|
||||
});
|
||||
}
|
||||
};
|
||||
170
gaia/apps/search/js/providers/local_apps.js
Executable file
170
gaia/apps/search/js/providers/local_apps.js
Executable file
|
|
@ -0,0 +1,170 @@
|
|||
/* global Search, DataGridProvider, GaiaGrid, Promise */
|
||||
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
function LocalApps() {
|
||||
this.apps = {};
|
||||
this.appListing = [];
|
||||
this.blacklist;
|
||||
|
||||
var mozApps = navigator.mozApps.mgmt;
|
||||
|
||||
mozApps.oninstall = e => {
|
||||
var app = e.application;
|
||||
var processApp = () => {
|
||||
this.apps[app.manifestURL] = app;
|
||||
this.createAppListing();
|
||||
};
|
||||
|
||||
if (app.downloading) {
|
||||
app.ondownloadapplied = processApp;
|
||||
} else {
|
||||
processApp();
|
||||
}
|
||||
};
|
||||
|
||||
mozApps.onuninstall = e => {
|
||||
delete this.apps[e.application.manifestURL];
|
||||
this.createAppListing();
|
||||
};
|
||||
|
||||
mozApps.getAll().onsuccess = e => {
|
||||
e.target.result.forEach(app => {
|
||||
this.apps[app.manifestURL] = app;
|
||||
});
|
||||
this.createBlacklist().then(() => {
|
||||
this.createAppListing();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
LocalApps.prototype = {
|
||||
|
||||
__proto__: DataGridProvider.prototype,
|
||||
|
||||
name: 'LocalApps',
|
||||
|
||||
dedupes: true,
|
||||
dedupeStrategy: 'exact',
|
||||
|
||||
createBlacklist: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var self = this;
|
||||
var key = 'app.launch_path.blacklist';
|
||||
var req = navigator.mozSettings.createLock().get(key);
|
||||
req.onsuccess = function onsuccess() {
|
||||
self.blacklist = req.result[key] || [];
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
createAppListing: function() {
|
||||
var appListing = [];
|
||||
var blacklist = this.blacklist;
|
||||
|
||||
var manifestURLs = Object.keys(this.apps);
|
||||
|
||||
manifestURLs.forEach(function eachManifest(manifestURL) {
|
||||
var app = this.apps[manifestURL];
|
||||
var manifest = app.manifest || app.updateManifest;
|
||||
|
||||
var HIDDEN_ROLES = [
|
||||
'system', 'input', 'homescreen', 'search', 'theme', 'addon',
|
||||
'langpack'
|
||||
];
|
||||
if (HIDDEN_ROLES.indexOf(manifest.role) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var entryPoints = manifest.entry_points;
|
||||
if (entryPoints) {
|
||||
for (var i in entryPoints) {
|
||||
var entry = entryPoints[i];
|
||||
entry.entryPoint = i;
|
||||
entry.manifestURL = app.manifestURL;
|
||||
appListing.push(entry);
|
||||
}
|
||||
} else {
|
||||
manifest.manifestURL = app.manifestURL;
|
||||
appListing.push(manifest);
|
||||
}
|
||||
|
||||
if (blacklist) {
|
||||
blacklist.forEach(function(blackentry) {
|
||||
for (var i in appListing) {
|
||||
var app = appListing[i];
|
||||
if (app.launch_path === blackentry) {
|
||||
appListing.splice(i, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.appListing = appListing;
|
||||
},
|
||||
|
||||
removeAppListing: function(app) {
|
||||
for (var i in this.appListing) {
|
||||
var manifest = this.appListing[i];
|
||||
if (manifest.manifestURL === app.manifestURL) {
|
||||
this.appListing.splice(i, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
search: function(input) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var results = this.find(input);
|
||||
var formatted = [];
|
||||
|
||||
results.forEach(function eachResult(result) {
|
||||
formatted.push({
|
||||
dedupeId: result.app.manifestURL,
|
||||
data: new GaiaGrid.Mozapp(result.app, result.entryPoint)
|
||||
});
|
||||
}, this);
|
||||
|
||||
resolve(formatted);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if an app manifest matches a query.
|
||||
*/
|
||||
matches: function(manifest, query) {
|
||||
query = query.toLowerCase();
|
||||
|
||||
// Get the localized name from the query.
|
||||
var shortName = manifest.short_name || '';
|
||||
var userLang = document.documentElement.lang;
|
||||
var locales = manifest.locales;
|
||||
var localized = locales && locales[userLang] && locales[userLang].name;
|
||||
localized = localized || '';
|
||||
|
||||
return shortName.toLowerCase().indexOf(query) != -1 ||
|
||||
manifest.name.toLowerCase().indexOf(query) != -1 ||
|
||||
localized.toLowerCase().indexOf(query) != -1;
|
||||
},
|
||||
|
||||
find: function(query) {
|
||||
var results = [];
|
||||
this.appListing.forEach(function(manifest) {
|
||||
if (this.matches(manifest, query)) {
|
||||
results.push({
|
||||
app: this.apps[manifest.manifestURL],
|
||||
entryPoint: manifest.entryPoint
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
Search.provider(new LocalApps());
|
||||
|
||||
}());
|
||||
85
gaia/apps/search/js/providers/marketplace.js
Executable file
85
gaia/apps/search/js/providers/marketplace.js
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
/* global Search, DataGridProvider, GaiaGrid, Promise */
|
||||
|
||||
(function() {
|
||||
|
||||
'use strict';
|
||||
|
||||
const NUM_DISPLAY = 4;
|
||||
var apiUrl = null;
|
||||
|
||||
function Marketplace() {}
|
||||
|
||||
Marketplace.prototype = {
|
||||
|
||||
__proto__: DataGridProvider.prototype,
|
||||
|
||||
name: 'Marketplace',
|
||||
|
||||
dedupes: true,
|
||||
dedupeStrategy: 'exact',
|
||||
remote: true,
|
||||
|
||||
init: function() {
|
||||
|
||||
DataGridProvider.prototype.init.apply(this, arguments);
|
||||
var urlKey = 'search.marketplace.url';
|
||||
var req = navigator.mozSettings.createLock().get(urlKey);
|
||||
req.onsuccess = function () {
|
||||
if (req.result[urlKey]) {
|
||||
apiUrl = req.result[urlKey]
|
||||
.replace('{limit}', NUM_DISPLAY)
|
||||
.replace('{lang}', document.documentElement.lang);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
search: function(input) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.abort();
|
||||
|
||||
if (!input || !apiUrl) {
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', apiUrl.replace('{q}', encodeURIComponent(input)), true);
|
||||
req.onload = (function onload() {
|
||||
var results = JSON.parse(req.responseText);
|
||||
if (!results.length) {
|
||||
resolve([]); return;
|
||||
}
|
||||
|
||||
var length = Math.min(NUM_DISPLAY, results.length);
|
||||
var formatted = [];
|
||||
for (var i = 0; i < length; i++) {
|
||||
var app = results[i];
|
||||
formatted.push({
|
||||
dedupeId: app.manifest_url,
|
||||
data: new GaiaGrid.MarketPlaceApp({
|
||||
id: app.manifest_url,
|
||||
name: navigator.mozL10n.get('install-marketplace-title', {
|
||||
title: app.name
|
||||
}),
|
||||
icons: app.icons,
|
||||
slug: app.slug
|
||||
})
|
||||
});
|
||||
}
|
||||
resolve(formatted);
|
||||
}).bind(this);
|
||||
req.onerror = function onerror() {
|
||||
console.log('Marketplace error.');
|
||||
reject();
|
||||
};
|
||||
req.ontimeout = function ontimeout() {
|
||||
console.log('Marketplace timeout.');
|
||||
reject();
|
||||
};
|
||||
req.send();
|
||||
this.request = req;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Search.provider(new Marketplace());
|
||||
}());
|
||||
400
gaia/apps/search/js/providers/places.js
Executable file
400
gaia/apps/search/js/providers/places.js
Executable file
|
|
@ -0,0 +1,400 @@
|
|||
/* globals Provider, SyncDataStore, Promise, IconsHelper */
|
||||
/* globals Search, PlacesIdbStore */
|
||||
/* globals DateHelper */
|
||||
/* globals asyncStorage */
|
||||
/* globals LazyLoader */
|
||||
(function(exports) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var _ = navigator.mozL10n.get;
|
||||
|
||||
// Maximum number of results to show show for a single query
|
||||
var MAX_AWESOME_RESULTS = 4;
|
||||
var MAX_HISTORY_RESULTS = 20;
|
||||
var MAX_TOPSITES_RESULTS = 6;
|
||||
|
||||
// Name of the datastore we pick up places from
|
||||
var STORE_NAME = 'places';
|
||||
|
||||
var topSitesWrapper = document.getElementById('top-sites');
|
||||
var historyWrapper = document.getElementById('history');
|
||||
|
||||
// These elements are only included in the newtab page.
|
||||
if (topSitesWrapper && historyWrapper) {
|
||||
topSitesWrapper.addEventListener('click', itemClicked);
|
||||
historyWrapper.addEventListener('click', itemClicked);
|
||||
}
|
||||
|
||||
var cachedLink = document.createElement('a');
|
||||
function parseUrl(url) {
|
||||
cachedLink.href = url;
|
||||
return cachedLink;
|
||||
}
|
||||
|
||||
// Storage for locally fetched icons
|
||||
var icons = {};
|
||||
var iconUrls = {};
|
||||
|
||||
function getIcon(place) {
|
||||
|
||||
if (place.url in icons && icons[place.url]) {
|
||||
return icons[place.url];
|
||||
}
|
||||
|
||||
IconsHelper.getIcon(place.url, null, place).then(icon => {
|
||||
saveIcon(place.url, icon);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function saveIcon(key, url) {
|
||||
if (key in icons) {
|
||||
return;
|
||||
}
|
||||
icons[key] = null;
|
||||
iconUrls[key] = url;
|
||||
fetchIcon(url, function(err, icon) {
|
||||
if (!err) {
|
||||
icons[key] = icon;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchIcon(uri, callback) {
|
||||
var xhr = new XMLHttpRequest({mozSystem: true});
|
||||
xhr.open('GET', uri, true);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.addEventListener('load', function() {
|
||||
// Check icon was successfully downloded
|
||||
// 0 is due to https://bugzilla.mozilla.org/show_bug.cgi?id=716491
|
||||
if (!(xhr.status === 200 || xhr.status === 0)) {
|
||||
return callback(new Error('error_downloading'));
|
||||
}
|
||||
|
||||
var blob = xhr.response;
|
||||
|
||||
if (blob.size > this.MAX_ICON_SIZE) {
|
||||
return callback(new Error('image_to_large'));
|
||||
}
|
||||
|
||||
// Only save the icon if it can be loaded as an image bigger than 0px
|
||||
var img = document.createElement('img');
|
||||
img.src = window.URL.createObjectURL(blob);
|
||||
|
||||
img.onload = function() {
|
||||
window.URL.revokeObjectURL(img.src);
|
||||
if (img.naturalWidth <= 0) {
|
||||
return callback(new Error('Cannot load image'));
|
||||
}
|
||||
callback(null, blob);
|
||||
};
|
||||
|
||||
img.onerror = function() {
|
||||
window.URL.revokeObjectURL(img.src);
|
||||
return callback(new Error('Cannot load image'));
|
||||
};
|
||||
});
|
||||
|
||||
xhr.onerror = function(err) {
|
||||
return callback(new Error('Cannot load uri'));
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function itemClicked(e) {
|
||||
if (e.target.dataset.url) {
|
||||
Search.navigate(e.target.dataset.url);
|
||||
}
|
||||
}
|
||||
|
||||
var listTemplate = createList();
|
||||
|
||||
function createList() {
|
||||
var list = document.createElement('ul');
|
||||
list.setAttribute('role', 'listbox');
|
||||
return list;
|
||||
}
|
||||
|
||||
function incrementHistoryThreshold(timestamp, currentThreshold, thresholds) {
|
||||
var newThreshold = currentThreshold += 1;
|
||||
if (timestamp < thresholds[newThreshold]) {
|
||||
return incrementHistoryThreshold(timestamp, newThreshold, thresholds);
|
||||
}
|
||||
return newThreshold;
|
||||
}
|
||||
|
||||
function drawHistoryHeading(parent, threshold, timestamp) {
|
||||
|
||||
var LABELS = [
|
||||
'future',
|
||||
'today',
|
||||
'yesterday',
|
||||
'last-7-days',
|
||||
'this-month',
|
||||
'last-6-months',
|
||||
'older-than-6-months'
|
||||
];
|
||||
|
||||
var text = '';
|
||||
|
||||
// Special case for month headings
|
||||
if (threshold == 5 && timestamp) {
|
||||
var date = new Date(timestamp);
|
||||
var now = new Date();
|
||||
text = _('month-' + date.getMonth());
|
||||
if (date.getFullYear() != now.getFullYear()) {
|
||||
text += ' ' + date.getFullYear();
|
||||
}
|
||||
} else {
|
||||
text = _(LABELS[threshold]);
|
||||
}
|
||||
|
||||
var h3 = document.createElement('h3');
|
||||
var spanNode = document.createElement('span');
|
||||
spanNode.textContent = text;
|
||||
var ul = listTemplate.cloneNode(true);
|
||||
h3.appendChild(spanNode);
|
||||
parent.appendChild(h3);
|
||||
parent.appendChild(ul);
|
||||
}
|
||||
|
||||
function updateIcon(visit, iconWrapper) {
|
||||
var iconDom = iconWrapper.querySelector('img');
|
||||
IconsHelper.getIcon(visit.url, null, visit).then((icon) => {
|
||||
if (icon && iconDom) {
|
||||
iconDom.onload = function () {
|
||||
iconWrapper.classList.remove('empty');
|
||||
iconDom.style.display = 'block';
|
||||
};
|
||||
iconDom.src = icon;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildHistory(visits) {
|
||||
|
||||
var thresholds = [
|
||||
Date.now(), // 0. Now
|
||||
DateHelper.todayStarted(), // 1. Today
|
||||
DateHelper.yesterdayStarted(), // 2. Yesterday
|
||||
DateHelper.thisWeekStarted(), // 3. This week
|
||||
DateHelper.thisMonthStarted(), // 4. This month
|
||||
DateHelper.lastSixMonthsStarted(), // 5. Six months
|
||||
0 // 6. Epoch!
|
||||
];
|
||||
|
||||
var threshold = 0;
|
||||
var month = null;
|
||||
var year = null;
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
visits.forEach(function(visit) {
|
||||
// Draw new heading if new threshold reached
|
||||
if (visit.date > 0 && visit.date < thresholds[threshold]) {
|
||||
threshold = incrementHistoryThreshold(visit.date,
|
||||
threshold, thresholds);
|
||||
// Special case for month headings
|
||||
if (threshold != 5) {
|
||||
drawHistoryHeading(fragment, threshold);
|
||||
}
|
||||
}
|
||||
|
||||
if (threshold === 5) {
|
||||
var timestampDate = new Date(visit.date);
|
||||
if (timestampDate.getMonth() != month ||
|
||||
timestampDate.getFullYear() != year) {
|
||||
month = timestampDate.getMonth();
|
||||
year = timestampDate.getFullYear();
|
||||
drawHistoryHeading(fragment, threshold, visit.date);
|
||||
}
|
||||
}
|
||||
|
||||
visit.meta = visit.url;
|
||||
visit.dataset = { url: visit.url };
|
||||
var dom = exports.Places.buildResultsDom([visit]);
|
||||
var iconDom = dom.querySelector('.icon');
|
||||
fragment.appendChild(dom);
|
||||
updateIcon(visit, iconDom);
|
||||
});
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function showStartPage() {
|
||||
|
||||
if (!topSitesWrapper || !historyWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
var store = exports.Places.persistStore;
|
||||
|
||||
var urls = [];
|
||||
store.readVisits(MAX_HISTORY_RESULTS, function(results) {
|
||||
var docFragment = buildHistory(results);
|
||||
historyWrapper.innerHTML = '';
|
||||
historyWrapper.appendChild(docFragment);
|
||||
}, function filter(visit) {
|
||||
var isStored = visit.url in urls;
|
||||
urls[visit.url] = true;
|
||||
return !isStored;
|
||||
});
|
||||
|
||||
store.read('frecency', MAX_TOPSITES_RESULTS, function(results) {
|
||||
var docFragment = document.createDocumentFragment();
|
||||
results.forEach(function(x) {
|
||||
docFragment.appendChild(formatTopResult(x));
|
||||
});
|
||||
topSitesWrapper.innerHTML = '';
|
||||
topSitesWrapper.appendChild(docFragment);
|
||||
});
|
||||
}
|
||||
|
||||
function formatTopResult(result) {
|
||||
var div = document.createElement('div');
|
||||
var span = document.createElement('span');
|
||||
if (result.title) {
|
||||
span.setAttribute('dir', 'auto');
|
||||
span.textContent = result.title;
|
||||
} else {
|
||||
span.setAttribute('dir', 'ltr');
|
||||
span.textContent = result.url;
|
||||
}
|
||||
div.dataset.url = result.url;
|
||||
div.classList.add('top-site');
|
||||
div.appendChild(span);
|
||||
div.setAttribute('role', 'link');
|
||||
|
||||
if (result.screenshot || result.tile) {
|
||||
var img = result.screenshot || result.tile;
|
||||
var imgUrl = (typeof img === 'string') ? img : URL.createObjectURL(img);
|
||||
div.style.backgroundImage = 'url(' + imgUrl + ')';
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
function matchesFilter(value, filter) {
|
||||
return !filter || (value && value.match(new RegExp(filter, 'i')) !== null);
|
||||
}
|
||||
|
||||
function formatPlace(placeObj, filter) {
|
||||
|
||||
var result = {
|
||||
'title': placeObj.title,
|
||||
'meta': placeObj.url,
|
||||
'icon': getIcon(placeObj),
|
||||
'dataset': {
|
||||
'url': placeObj.url
|
||||
},
|
||||
'label': placeObj.title
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function Places() {}
|
||||
|
||||
Places.prototype = {
|
||||
|
||||
__proto__: Provider.prototype,
|
||||
|
||||
name: 'Places',
|
||||
|
||||
click: itemClicked,
|
||||
|
||||
init: function() {
|
||||
|
||||
// If not on new tab page, set places div as container
|
||||
if (!topSitesWrapper || !historyWrapper) {
|
||||
this.header = document.getElementById(this.name.toLowerCase() +
|
||||
'-header');
|
||||
this.container = document.getElementById(this.name.toLowerCase());
|
||||
this.container.addEventListener('click', this.click.bind(this));
|
||||
}
|
||||
|
||||
this.persistStore = new PlacesIdbStore();
|
||||
|
||||
this.persistStore.init().then(() => {
|
||||
|
||||
this.syncStore =
|
||||
new SyncDataStore(STORE_NAME, this.persistStore, 'url');
|
||||
|
||||
this.syncStore.filter = function(place) {
|
||||
return place.url.startsWith('app://') ||
|
||||
place.url === 'about:blank';
|
||||
};
|
||||
this.syncStore.onChange = function() {
|
||||
showStartPage();
|
||||
};
|
||||
// Make init return a promise, so we know when
|
||||
// we did the sync. Used right now for testing
|
||||
// porpoises.
|
||||
var rev = this.persistStore.latestRevision || 0;
|
||||
return this.syncStore.sync(rev).then(() => {
|
||||
return new Promise(resolve => {
|
||||
|
||||
function done() {
|
||||
showStartPage();
|
||||
resolve();
|
||||
}
|
||||
|
||||
asyncStorage.getItem('have-preloaded-sites', (havePreloaded) => {
|
||||
if (!havePreloaded) {
|
||||
this.preloadTopSites().then(() => {
|
||||
asyncStorage.setItem('have-preloaded-sites', true);
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
saveSites: function(sites) {
|
||||
return Promise.all(sites.map(site => {
|
||||
site.frecency = -2;
|
||||
return this.persistStore.addPlace(site);
|
||||
}));
|
||||
},
|
||||
|
||||
preloadTopSites: function() {
|
||||
// load default build top sites
|
||||
return LazyLoader.getJSON('/js/inittopsites.json').then(sites => {
|
||||
return this.saveSites(sites);
|
||||
});
|
||||
},
|
||||
|
||||
search: function(filter) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var matchedOrigins = {};
|
||||
this.persistStore.read('frecency', MAX_AWESOME_RESULTS, (results) => {
|
||||
resolve(results.map(function(result) {
|
||||
return formatPlace(result, filter);
|
||||
}));
|
||||
}, function filterFun(result) {
|
||||
if (result.frecency <= 0) {
|
||||
return false;
|
||||
}
|
||||
var url = parseUrl(result.url);
|
||||
var matches = !(url.hostname in matchedOrigins) &&
|
||||
(matchesFilter(result.title, filter) ||
|
||||
matchesFilter(result.url, filter));
|
||||
if (matches) {
|
||||
matchedOrigins[url.hostname] = true;
|
||||
}
|
||||
return matches;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.Places = new Places();
|
||||
Search.provider(exports.Places);
|
||||
|
||||
}(window));
|
||||
168
gaia/apps/search/js/providers/provider.js
Executable file
168
gaia/apps/search/js/providers/provider.js
Executable file
|
|
@ -0,0 +1,168 @@
|
|||
/* globals IconsHelper */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Base Provider class
|
||||
*/
|
||||
function Provider() {
|
||||
}
|
||||
|
||||
Provider.prototype = {
|
||||
|
||||
/**
|
||||
* Name of the provider
|
||||
* Overridden at the child provider level
|
||||
*/
|
||||
name: 'Provider',
|
||||
|
||||
/**
|
||||
* Whether or not this provider dedupes results.
|
||||
*/
|
||||
dedupes: false,
|
||||
|
||||
/**
|
||||
* Initializes the provider container and adds listeners
|
||||
*/
|
||||
init: function(searchObj) {
|
||||
this.searchObj = searchObj;
|
||||
this.container = document.getElementById(this.name.toLowerCase());
|
||||
this.container.addEventListener('click', this.click.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the rendered results of this provider from the app grid
|
||||
*/
|
||||
clear: function() {
|
||||
this.container.innerHTML = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler when a result is clicked
|
||||
*/
|
||||
click: function() {},
|
||||
|
||||
/**
|
||||
* Aborts any in-progress request.
|
||||
*/
|
||||
abort: function() {
|
||||
if (this.request && this.request.abort) {
|
||||
this.request.abort();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an icon once it has been loaded.
|
||||
*
|
||||
* @param {Object} config A configuration object.
|
||||
* @param {Object} iconWrapper An iconWrapper DOM object.
|
||||
*/
|
||||
updateIcon: function(config, iconWrapper) {
|
||||
var iconDom = iconWrapper.querySelector('img');
|
||||
IconsHelper.getIcon(config.dataset.url, null, config).then((icon) => {
|
||||
if (icon && iconDom) {
|
||||
iconDom.onload = function () {
|
||||
iconWrapper.classList.remove('empty');
|
||||
iconDom.style.display = 'block';
|
||||
};
|
||||
iconDom.src = icon;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a set of results.
|
||||
* Each result may contain the following attributes:
|
||||
* - title: The title of the app.
|
||||
* - meta: Secondary content to show for the result.
|
||||
* - icon: The icon of the result.
|
||||
* - dataset: Data attributes to apply to the result.
|
||||
* - label: Aria-label for the result.
|
||||
* - description: Additional description for the result.
|
||||
*/
|
||||
buildResultsDom: function(results) {
|
||||
//<div class="result" data-url="mozilla.com" role="link"
|
||||
// aria-label="My Urlasljd alskdja lsdjka sldjk"
|
||||
// aria-describedby="description-0">
|
||||
// <div class="icon">
|
||||
// <img role="presentation" src="..." />
|
||||
// </div>
|
||||
// <div class="urlwrapper">
|
||||
// <span class="title">My Urlasljd <span class="highlight">alskd</span>
|
||||
// ja lsdjka sldjk</span>
|
||||
// <small id="description-0" class="url"
|
||||
// aria-label="http://url.com">http://url.com</small>
|
||||
// </div>
|
||||
//</div>
|
||||
|
||||
var frag = document.createDocumentFragment();
|
||||
results.forEach(function(config, index) {
|
||||
|
||||
var result = document.createElement('div');
|
||||
var iconWrapper = document.createElement('div');
|
||||
var icon = document.createElement('img');
|
||||
var description = document.createElement('div');
|
||||
var title = document.createElement('span');
|
||||
var meta = document.createElement('small');
|
||||
|
||||
result.classList.add('result');
|
||||
iconWrapper.classList.add('icon');
|
||||
description.classList.add('description');
|
||||
title.classList.add('title');
|
||||
meta.classList.add('meta');
|
||||
|
||||
for (var i in config.dataset) {
|
||||
result.dataset[i] = config.dataset[i];
|
||||
}
|
||||
|
||||
if (config.title) {
|
||||
title.setAttribute('dir', 'auto');
|
||||
title.textContent = config.title;
|
||||
} else {
|
||||
title.setAttribute('dir', 'ltr');
|
||||
title.textContent = config.url;
|
||||
}
|
||||
|
||||
if (config.meta) {
|
||||
meta.textContent = config.meta;
|
||||
// Expose meta infrormation as a helpful description for each result.
|
||||
if (config.description) {
|
||||
meta.id = this.name + '-description-' + index;
|
||||
meta.setAttribute('aria-label', config.description);
|
||||
result.setAttribute('aria-describedby', meta.id);
|
||||
}
|
||||
}
|
||||
|
||||
icon.setAttribute('role', 'presentation');
|
||||
if (config.icon) {
|
||||
icon.src = window.URL.createObjectURL(config.icon);
|
||||
} else {
|
||||
iconWrapper.classList.add('empty');
|
||||
}
|
||||
|
||||
result.setAttribute('role', 'link');
|
||||
// Either use an explicit label or, if not present, title.
|
||||
result.setAttribute('aria-label', config.label || config.title);
|
||||
|
||||
description.appendChild(title);
|
||||
description.appendChild(meta);
|
||||
iconWrapper.appendChild(icon);
|
||||
result.appendChild(iconWrapper);
|
||||
result.appendChild(description);
|
||||
frag.appendChild(result);
|
||||
|
||||
if (!config.icon) {
|
||||
this.updateIcon(config, iconWrapper);
|
||||
}
|
||||
}, this);
|
||||
return frag;
|
||||
},
|
||||
|
||||
render: function(results) {
|
||||
var dom = this.buildResultsDom(results);
|
||||
this.container.appendChild(dom);
|
||||
if (this.header) {
|
||||
results.length ? this.header.classList.remove('hidden') :
|
||||
this.header.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
};
|
||||
140
gaia/apps/search/js/providers/suggestions.js
Executable file
140
gaia/apps/search/js/providers/suggestions.js
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
(function() {
|
||||
|
||||
'use strict';
|
||||
/* globals Promise, Provider, Search, MetricsHelper */
|
||||
/* globals LazyLoader */
|
||||
/* globals SearchProvider */
|
||||
|
||||
var suggestionsWrapper = document.getElementById('suggestions-wrapper');
|
||||
var suggestionsProvider = document.getElementById('suggestions-provider');
|
||||
var suggestionsSelect = document.getElementById('suggestions-select');
|
||||
|
||||
function providerUpdated() {
|
||||
|
||||
navigator.mozL10n.setAttributes(suggestionsProvider, 'search-header', {
|
||||
provider: SearchProvider('title').toUpperCase()
|
||||
});
|
||||
|
||||
var providers = SearchProvider.providers();
|
||||
var selectFragment = document.createDocumentFragment();
|
||||
var optionNode = document.createElement('option');
|
||||
|
||||
Object.keys(providers).forEach(provider => {
|
||||
var option = optionNode.cloneNode();
|
||||
option.value = provider;
|
||||
option.text = providers[provider].title;
|
||||
if (provider === SearchProvider.selected()) {
|
||||
option.selected = true;
|
||||
}
|
||||
selectFragment.appendChild(option);
|
||||
});
|
||||
|
||||
suggestionsSelect.innerHTML = '';
|
||||
suggestionsSelect.appendChild(selectFragment);
|
||||
}
|
||||
|
||||
function encodeTerms(str, search) {
|
||||
return str.replace('{searchTerms}', encodeURIComponent(search));
|
||||
}
|
||||
|
||||
function Suggestions() {}
|
||||
|
||||
Suggestions.prototype = {
|
||||
|
||||
__proto__: Provider.prototype,
|
||||
|
||||
name: 'Suggestions',
|
||||
|
||||
currentSearch: null,
|
||||
|
||||
init: function() {
|
||||
this.metrics = new MetricsHelper();
|
||||
this.metrics.init();
|
||||
|
||||
Provider.prototype.init.apply(this, arguments);
|
||||
|
||||
suggestionsSelect.addEventListener('change', function(e) {
|
||||
SearchProvider.setProvider(e.target.value);
|
||||
});
|
||||
|
||||
SearchProvider.ready().then(() => {
|
||||
providerUpdated();
|
||||
SearchProvider.providerUpdated(() => {
|
||||
providerUpdated();
|
||||
this.redoSearch();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
redoSearch: function() {
|
||||
if (!this.currentSearch) {
|
||||
return;
|
||||
}
|
||||
this.search(this.currentSearch).then(results => {
|
||||
this.render(results);
|
||||
});
|
||||
},
|
||||
|
||||
click: function(e) {
|
||||
this.metrics.report('websearch', SearchProvider('title'));
|
||||
var suggestion = e.target.dataset.suggestion;
|
||||
var url = encodeTerms(SearchProvider('searchUrl'), suggestion);
|
||||
Search.navigate(url);
|
||||
},
|
||||
|
||||
search: function(input, preventRemote) {
|
||||
|
||||
this.currentSearch = preventRemote ? null : input;
|
||||
this.render([input]);
|
||||
|
||||
if (!navigator.onLine || preventRemote) {
|
||||
return Promise.resolve([input]);
|
||||
}
|
||||
|
||||
suggestionsWrapper.dataset.loading = true;
|
||||
|
||||
return SearchProvider.ready().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
var url = encodeTerms(SearchProvider('suggestUrl'), input);
|
||||
LazyLoader.getJSON(url, true).then(result => {
|
||||
var results = result[1];
|
||||
// We add an item to search the entered term as well
|
||||
results.unshift(input);
|
||||
resolve(results);
|
||||
}).catch(function() {
|
||||
suggestionsWrapper.dataset.loading = false;
|
||||
reject();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function(items) {
|
||||
var ul = document.createElement('ul');
|
||||
ul.setAttribute('role', 'listbox');
|
||||
|
||||
items.forEach(function each(item) {
|
||||
var li = document.createElement('li');
|
||||
li.dataset.suggestion = li.textContent = item;
|
||||
li.setAttribute('role', 'option');
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
suggestionsWrapper.dataset.loading = false;
|
||||
this.container.innerHTML = '';
|
||||
|
||||
if (ul.childNodes.length) {
|
||||
this.container.appendChild(ul);
|
||||
}
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
Provider.prototype.clear.apply(this, arguments);
|
||||
this.currentSearch = null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Search.provider(new Suggestions());
|
||||
|
||||
}());
|
||||
Loading…
Reference in a new issue