VulpesOS/gaia/apps/calendar/js/views/settings.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

358 lines
11 KiB
JavaScript
Executable file

define(function(require, exports, module) {
'use strict';
var CalendarTemplate = require('templates/calendar');
var View = require('view');
var co = require('ext/co');
var core = require('core');
var debug = require('common/debug')('views/settings');
var router = require('router');
require('dom!settings');
function Settings(options) {
View.apply(this, arguments);
this.calendarList = [];
this._hideSettings = this._hideSettings.bind(this);
this._onDrawerTransitionEnd = this._onDrawerTransitionEnd.bind(this);
this._observeUI();
}
module.exports = Settings;
Settings.prototype = {
__proto__: View.prototype,
calendarList: null,
/**
* used internally to check if calendar and account data was loaded from db
* @type {Promise}
* @private
*/
_load: null,
waitBeforePersist: 600,
/**
* Local update is a flag
* used to indicate that the incoming
* update was made by this view and
* should not fire the _update method.
*/
_localUpdate: false,
/**
* Name of the class that will be applied to the
* body element when sync is in progress.
*/
selectors: {
element: '#settings',
calendars: '#settings .calendars',
calendarName: '.name',
toolbar: '#settings [role="tablist"]',
header: '#settings-header',
headerTitle: '#settings-header h1',
// A dark semi-opaque layer that is used to "gray out" the view behind
// the element used for settings. Tapping on it will also close out
// the settings element, per ux desire.
shield: '#settings .settings-shield',
// This outer div is used to hide .settings-drawer via an
// overflow: hidden, so that the .settings-drawer can translateY
// animate downward and appear to come out from under the view
// header that is visible "behind" the element used for settings.
drawerContainer: '#settings .settings-drawer-container',
// Holds the actual visible drawer contents: list of calendars
// and bottom toolbar.
drawer: '#settings .settings-drawer',
advancedSettingsButton: '#settings .settings',
syncButton: '#settings .sync',
syncProgress: '#settings .sync-progress',
// A view that settings overlays. Still needs to be active/visible but
// hidden from the screen reader.
timeViews: '#time-views'
},
get calendars() {
return this._findElement('calendars');
},
get toolbar() {
return this._findElement('toolbar');
},
get header() {
return this._findElement('header');
},
get headerTitle() {
return this._findElement('headerTitle');
},
get shield() {
return this._findElement('shield');
},
get drawerContainer() {
return this._findElement('drawerContainer');
},
get drawer() {
return this._findElement('drawer');
},
get advancedSettingsButton() {
return this._findElement('advancedSettingsButton');
},
get syncButton() {
return this._findElement('syncButton');
},
get syncProgress() {
return this._findElement('syncProgress');
},
get timeViews() {
return this._findElement('timeViews');
},
_syncStartStatus: function () {
this.syncProgress.setAttribute('data-l10n-id', 'sync-progress-syncing');
this.syncProgress.classList.add('syncing');
},
_syncCompleteStatus: function () {
this.syncProgress.setAttribute('data-l10n-id', 'sync-progress-complete');
this.syncProgress.classList.remove('syncing');
},
_observeUI: function() {
this.advancedSettingsButton.addEventListener('click', function(e) {
e.stopPropagation();
router.show('/advanced-settings/');
});
this.syncButton.addEventListener('click', this._onSyncClick.bind(this));
core.syncController.on('syncStart', this._syncStartStatus.bind(this));
core.syncController.on('syncComplete',
this._syncCompleteStatus.bind(this));
this.calendars.addEventListener(
'change', this._onCalendarDisplayToggle.bind(this)
);
},
_observeAccounts: function() {
var stream = core.bridge.observeAccounts();
stream.listen(this._updateSyncButton.bind(this));
return new Promise(resolve => stream.listen(resolve));
},
_observeCalendars: function() {
var stream = core.bridge.observeCalendars();
stream.listen(this._updateCalendars.bind(this));
return new Promise(resolve => stream.listen(resolve));
},
_updateCalendars: function(records) {
this.calendarList = records.map(r => r.calendar);
this.render();
},
_persistCalendarDisplay: co.wrap(function *(id, displayed) {
try {
var calendar = this.calendarList.find(c => String(c._id) === String(id));
// we "clone" the calendar otherwise the caching logic will affect the
// 'calendarVisibilityChange' event (we check if value changed this way)
calendar = Object.create(calendar);
calendar.localDisplayed = displayed;
yield core.bridge.updateCalendar(calendar);
} catch (err) {
console.error(`Can't save calendar display: ${err}`);
}
}),
_onCalendarDisplayToggle: function(e) {
var input = e.target;
this._persistCalendarDisplay(input.getAttribute('value'), !!input.checked);
},
_onSyncClick: function() {
// trigger the sync the syncStart/complete events
// will hide/show the button.
core.syncController.all();
},
// Ajust size of drawer scroll area to fit size of calendars, within
// a min/max that is controlled by CSS. This has to be a manual
// calculation because UX wants the list of calendars to form-fit
// without a scrollbar, but enforce a minimum height and a maximum.
// The alternative to this approach is to size drawerContainer and
// drawer to be height 100%, and put the min/max height CSS on the
// .calendars. However, that means the translate animation is over
// a 100% height div, which ends up looking not so smooth on close
// of the animation, since the actual visible content is about half
// the size of that 100% and in the easing, zips by too quickly that
// it is harder to track, almost looks like just a harder visibility
// discontinuity.
_setCalendarContainerSize: function() {
var nodes = this.calendars.children;
var calendarsHeight = nodes[0] ?
nodes[0].getBoundingClientRect().height *
nodes.length : 0;
this.drawerContainer.style.height = (calendarsHeight +
this.toolbar.clientHeight) + 'px';
},
onrender: function() {
this._setCalendarContainerSize();
this._rendered = true;
this._animateDrawer();
},
render: function() {
debug('Will render settings view.');
this.calendars.innerHTML = '';
debug('Inject calendars into settings list.');
this.calendarList.forEach(calendar => {
debug('Will add calendar to settings view', calendar.id, calendar);
var html = CalendarTemplate.item.render(calendar);
this.calendars.insertAdjacentHTML('beforeend', html);
if (calendar.error) {
console.error('Views.Settings calendar error:', calendar.error);
var idx = this.calendars.children.length - 1;
var el = this.calendars.children[idx];
el.classList.add('error');
}
this._setCalendarContainerSize();
});
this.onrender && this.onrender();
},
_updateSyncButton: function(list) {
var canSync = list.some(o => o.provider.canSync);
this.toolbar.classList.toggle('noaccount', !canSync);
},
_onDrawerTransitionEnd: function(e) {
this._updateDrawerAnimState('done');
if (!document.body.classList.contains('settings-drawer-visible')) {
router.resetState();
}
},
// Update a state visible in the DOM for when animation is taking place.
// This is mostly useful for a test hook to know when the animation is
// done.
_updateDrawerAnimState: function(state) {
this.drawer.dataset.animstate = state;
},
_hideSettings: function() {
this._updateDrawerAnimState('animating');
document.body.classList.remove('settings-drawer-visible');
},
_animateDrawer: function() {
// Wait for both _rendered and _activated before triggering
// the animation, so that it is smooth, without jank due to
// changes in style/layout from activating or rendering.
// Also, set the style on the body, since other views will also
// have items animate based on the class. For instance, the +
// to add an event in the view-selector views fades out.
if (!this._rendered) {
return debug('Skip animation since not yet rendered.');
}
if (!this._activated) {
return debug('Skip animation since not yet activated.');
}
var classList = document.body.classList;
if (classList.contains('settings-drawer-visible')) {
return debug('Skip animation since drawer already visible?');
}
this._updateDrawerAnimState('animating');
classList.add('settings-drawer-visible');
},
_loadBaseData: function() {
if (!this._load) {
// make sure we only observe data once and wait until it's ready before
// showing the animation
this._load = Promise.all([
this._observeCalendars(),
this._observeAccounts()
]);
}
return this._load;
},
onactive: co.wrap(function *() {
debug('Will do settings animation.');
try {
yield this._loadBaseData();
} catch(err) {
console.error('Error fetching calendars in View.Settings', err);
return;
}
// View#onactive will call Views.Settings#render the first time.
View.prototype.onactive.apply(this, arguments);
// onactive can be called more times than oninactive, since
// settings can overlay over and not trigger an inactive state,
// so only bind these listeners and do the drawer animation once.
if (document.body.classList.contains('settings-drawer-visible')) {
return;
}
debug('Settings drawer is not visible... will activate.');
this._activated = true;
this._animateDrawer();
// Set header title to same as time view header
this.headerTitle.textContent =
document.getElementById('current-month-year').textContent;
// Both the transparent back and clicking on the semi-opaque
// shield should close the settings since visually those sections
// do not look like part of the drawer UI, and UX wants to give
// the user a few options to close the drawer since there is no
// explicit close button.
this.header.addEventListener('action', this._hideSettings);
this.shield.addEventListener('click', this._hideSettings);
this.timeViews.setAttribute('aria-hidden', true);
this.drawer.addEventListener('transitionend', this._onDrawerTransitionEnd);
}),
oninactive: function() {
debug('Will deactivate settings.');
View.prototype.oninactive.apply(this, arguments);
this._activated = false;
this.header.removeEventListener('action', this._hideSettings);
this.shield.removeEventListener('click', this._hideSettings);
this.timeViews.removeAttribute('aria-hidden');
this.drawer.removeEventListener('transitionend',
this._onDrawerTransitionEnd);
}
};
Settings.prototype.onfirstseen = Settings.prototype.render;
});