Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
400 lines
11 KiB
JavaScript
Executable file
400 lines
11 KiB
JavaScript
Executable file
/* 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));
|