mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-10-30 14:25:42 +00:00
(fix) updates for new packages
This commit is contained in:
1 parent
91e4c2cefc
commit
0a8834193f
24 files changed
+679
-361
No files matched your search
+288
-78
@@ -2,7 +2,7 @@
|
||||
* Angular Material Design
|
||||
* https://github.com/angular/material
|
||||
* @license MIT
|
||||
* v0.10.1-master-8c71d0c
|
||||
* v0.10.1-master-c9f2b9f
|
||||
*/
|
||||
(function( window, angular, undefined ){
|
||||
"use strict";
|
||||
@@ -892,6 +892,22 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
|
||||
return $mdUtil.now() - start;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create an implicit getter that caches its `getter()`
|
||||
* lookup value
|
||||
*/
|
||||
valueOnUse : function (scope, key, getter) {
|
||||
var value = null, args = Array.prototype.slice.call(arguments);
|
||||
var params = (args.length > 3) ? args.slice(3) : [ ];
|
||||
|
||||
Object.defineProperty(scope, key, {
|
||||
get: function () {
|
||||
if (value === null) value = getter.apply(scope, params);
|
||||
return value;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a unique ID.
|
||||
*
|
||||
@@ -1347,6 +1363,12 @@ mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controlle
|
||||
*/
|
||||
var pointer, lastPointer, forceSkipClickHijack = false;
|
||||
|
||||
/**
|
||||
* The position of the most recent click if that click was on a label element.
|
||||
* @type {{x: number, y: number}?}
|
||||
*/
|
||||
var lastLabelClickPos = null;
|
||||
|
||||
// Used to attach event listeners once when multiple ng-apps are running.
|
||||
var isInitialized = false;
|
||||
|
||||
@@ -1790,9 +1812,16 @@ mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controlle
|
||||
*/
|
||||
document.addEventListener('click', function clickHijacker(ev) {
|
||||
var isKeyClick = ev.clientX === 0 && ev.clientY === 0;
|
||||
if (!isKeyClick && !ev.$material && !ev.isIonicTap) {
|
||||
if (!isKeyClick && !ev.$material && !ev.isIonicTap
|
||||
&& !isInputEventFromLabelClick(ev)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
lastLabelClickPos = null;
|
||||
} else {
|
||||
lastLabelClickPos = null;
|
||||
if (ev.target.tagName.toLowerCase() == 'label') {
|
||||
lastLabelClickPos = {x: ev.x, y: ev.y};
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
@@ -1913,6 +1942,28 @@ mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controlle
|
||||
return ev && pointer && ev.type.charAt(0) === pointer.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the given event is an input event that was caused by clicking on an
|
||||
* associated label element.
|
||||
*
|
||||
* This is necessary because the browser will, upon clicking on a label element, fire an
|
||||
* *extra* click event on its associated input (if any). mdGesture is able to flag the label
|
||||
* click as with `$material` correctly, but not the second input click.
|
||||
*
|
||||
* In order to determine whether an input event is from a label click, we compare the (x, y) for
|
||||
* the event to the (x, y) for the most recent label click (which is cleared whenever a non-label
|
||||
* click occurs). Unfortunately, there are no event properties that tie the input and the label
|
||||
* together (such as relatedTarget).
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInputEventFromLabelClick(event) {
|
||||
return lastLabelClickPos
|
||||
&& lastLabelClickPos.x == event.x
|
||||
&& lastLabelClickPos.y == event.y;
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the given pointer based upon the given DOMEvent.
|
||||
* Distance, velocity, direction, duration, etc
|
||||
@@ -2876,12 +2927,13 @@ function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdU
|
||||
this.options = rippleOptions;
|
||||
this.mousedown = false;
|
||||
this.ripples = [];
|
||||
this.container = null;
|
||||
this.color = null;
|
||||
this.background = null;
|
||||
this.timeout = null; // Stores a reference to the most-recent ripple timeout
|
||||
this.lastRipple = null;
|
||||
|
||||
$mdUtil.valueOnUse(this, 'container', this.createContainer);
|
||||
$mdUtil.valueOnUse(this, 'color', this.getColor, 1);
|
||||
$mdUtil.valueOnUse(this, 'background', this.getColor, 0.5);
|
||||
|
||||
// attach method for unit tests
|
||||
($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple);
|
||||
|
||||
@@ -3015,10 +3067,6 @@ InkRippleCtrl.prototype.clearTimeout = function () {
|
||||
* @param top
|
||||
*/
|
||||
InkRippleCtrl.prototype.createRipple = function (left, top) {
|
||||
if (!this.container) this.container = this.createContainer();
|
||||
if (!this.color) this.color = this.getColor();
|
||||
if (!this.background) this.background = this.getColor(0.5);
|
||||
|
||||
var ctrl = this;
|
||||
var ripple = angular.element('<div class="md-ripple"></div>');
|
||||
var width = this.$element.prop('clientWidth');
|
||||
@@ -3173,8 +3221,8 @@ angular.module('material.core.theming.palette', [])
|
||||
'A400': '#ff1744',
|
||||
'A700': '#d50000',
|
||||
'contrastDefaultColor': 'light',
|
||||
'contrastDarkColors': '50 100 200 300 400 A100',
|
||||
'contrastStrongLightColors': '500 600 700 A200 A400 A700'
|
||||
'contrastDarkColors': '50 100 200 300 A100',
|
||||
'contrastStrongLightColors': '400 500 600 700 A200 A400 A700'
|
||||
},
|
||||
'pink': {
|
||||
'50': '#fce4ec',
|
||||
@@ -3192,7 +3240,7 @@ angular.module('material.core.theming.palette', [])
|
||||
'A400': '#f50057',
|
||||
'A700': '#c51162',
|
||||
'contrastDefaultColor': 'light',
|
||||
'contrastDarkColors': '50 100 200 300 400 A100',
|
||||
'contrastDarkColors': '50 100 200 A100',
|
||||
'contrastStrongLightColors': '500 600 A200 A400 A700'
|
||||
},
|
||||
'purple': {
|
||||
@@ -3268,7 +3316,7 @@ angular.module('material.core.theming.palette', [])
|
||||
'A400': '#2979ff',
|
||||
'A700': '#2962ff',
|
||||
'contrastDefaultColor': 'light',
|
||||
'contrastDarkColors': '100 200 300 400 A100',
|
||||
'contrastDarkColors': '50 100 200 300 400 A100',
|
||||
'contrastStrongLightColors': '500 600 700 A200 A400 A700'
|
||||
},
|
||||
'light-blue': {
|
||||
@@ -3287,8 +3335,8 @@ angular.module('material.core.theming.palette', [])
|
||||
'A400': '#00b0ff',
|
||||
'A700': '#0091ea',
|
||||
'contrastDefaultColor': 'dark',
|
||||
'contrastLightColors': '500 600 700 800 900 A700',
|
||||
'contrastStrongLightColors': '500 600 700 800 A700'
|
||||
'contrastLightColors': '600 700 800 900 A700',
|
||||
'contrastStrongLightColors': '600 700 800 A700'
|
||||
},
|
||||
'cyan': {
|
||||
'50': '#e0f7fa',
|
||||
@@ -3306,8 +3354,8 @@ angular.module('material.core.theming.palette', [])
|
||||
'A400': '#00e5ff',
|
||||
'A700': '#00b8d4',
|
||||
'contrastDefaultColor': 'dark',
|
||||
'contrastLightColors': '500 600 700 800 900',
|
||||
'contrastStrongLightColors': '500 600 700 800'
|
||||
'contrastLightColors': '700 800 900',
|
||||
'contrastStrongLightColors': '700 800 900'
|
||||
},
|
||||
'teal': {
|
||||
'50': '#e0f2f1',
|
||||
@@ -3344,8 +3392,8 @@ angular.module('material.core.theming.palette', [])
|
||||
'A400': '#00e676',
|
||||
'A700': '#00c853',
|
||||
'contrastDefaultColor': 'dark',
|
||||
'contrastLightColors': '500 600 700 800 900',
|
||||
'contrastStrongLightColors': '500 600 700'
|
||||
'contrastLightColors': '600 700 800 900',
|
||||
'contrastStrongLightColors': '600 700'
|
||||
},
|
||||
'light-green': {
|
||||
'50': '#f1f8e9',
|
||||
@@ -3363,8 +3411,8 @@ angular.module('material.core.theming.palette', [])
|
||||
'A400': '#76ff03',
|
||||
'A700': '#64dd17',
|
||||
'contrastDefaultColor': 'dark',
|
||||
'contrastLightColors': '800 900',
|
||||
'contrastStrongLightColors': '800 900'
|
||||
'contrastLightColors': '700 800 900',
|
||||
'contrastStrongLightColors': '700 800 900'
|
||||
},
|
||||
'lime': {
|
||||
'50': '#f9fbe7',
|
||||
@@ -5387,7 +5435,7 @@ angular
|
||||
* </hljs>
|
||||
*
|
||||
*/
|
||||
function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) {
|
||||
function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) {
|
||||
inputDirective = inputDirective[0];
|
||||
var CHECKED_CSS = 'md-checked';
|
||||
|
||||
@@ -5494,7 +5542,7 @@ function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant,
|
||||
};
|
||||
}
|
||||
}
|
||||
MdCheckboxDirective.$inject = ["inputDirective", "$mdInkRipple", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"];
|
||||
MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"];
|
||||
|
||||
})();
|
||||
(function(){
|
||||
@@ -5953,7 +6001,7 @@ function iosScrollFix(node) {
|
||||
* @param {Date=} opt_date
|
||||
*/
|
||||
CalendarCtrl.prototype.focus = function(opt_date) {
|
||||
var date = opt_date || this.selectedDate;
|
||||
var date = opt_date || this.selectedDate || this.today;
|
||||
|
||||
var previousFocus = this.calendarElement.querySelector('.md-focus');
|
||||
if (previousFocus) {
|
||||
@@ -6114,7 +6162,7 @@ function iosScrollFix(node) {
|
||||
// of repeated items that are linked, and then those elements have their bindings updataed.
|
||||
// Since the months are not generated by bindings, we simply regenerate the entire thing
|
||||
// when the binding (offset) changes.
|
||||
scope.$watch(function() { return monthCtrl.offset }, function(offset, oldOffset) {
|
||||
scope.$watch(function() { return monthCtrl.offset; }, function(offset, oldOffset) {
|
||||
if (offset != oldOffset) {
|
||||
monthCtrl.generateContent();
|
||||
}
|
||||
@@ -6143,11 +6191,11 @@ function iosScrollFix(node) {
|
||||
this.calendarCtrl = null;
|
||||
|
||||
/**
|
||||
* Number of months from the start of the month "items"
|
||||
* that the currently rendered month occurs.
|
||||
* Number of months from the start of the month "items" that the currently rendered month
|
||||
* occurs. Set via angular data binding.
|
||||
* @type {number}
|
||||
*/
|
||||
this.offset = 0;
|
||||
this.offset;
|
||||
|
||||
/**
|
||||
* Date cell to focus after appending the month to the document.
|
||||
@@ -6479,7 +6527,7 @@ function iosScrollFix(node) {
|
||||
* @returns {string}
|
||||
*/
|
||||
function defaultFormatDate(date) {
|
||||
return date.toLocaleDateString();
|
||||
return date ? date.toLocaleDateString() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6609,6 +6657,7 @@ function iosScrollFix(node) {
|
||||
* @module material.components.datepicker
|
||||
*
|
||||
* @param {Date} ng-model The component's model. Expects a JavaScript Date object.
|
||||
* @param {expression=} ng-change Expression evaluated when the model value changes.
|
||||
* @param {boolean=} disabled Whether the datepicker is disabled.
|
||||
*
|
||||
* @description
|
||||
@@ -6637,7 +6686,8 @@ function iosScrollFix(node) {
|
||||
'ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' +
|
||||
'<input class="md-datepicker-input" aria-haspopup="true" ' +
|
||||
'ng-focus="ctrl.setFocused(true)" ng-blur="ctrl.setFocused(false)">' +
|
||||
'<md-button md-no-ink class="md-datepicker-triangle-button md-icon-button" ' +
|
||||
'<md-button type="button" md-no-ink ' +
|
||||
'class="md-datepicker-triangle-button md-icon-button" ' +
|
||||
'ng-click="ctrl.openCalendarPane($event)" ' +
|
||||
'aria-label="{{::ctrl.dateLocale.msgOpenCalendar}}">' +
|
||||
'<div class="md-datepicker-expand-triangle"></div>' +
|
||||
@@ -6646,7 +6696,9 @@ function iosScrollFix(node) {
|
||||
|
||||
// This pane will be detached from here and re-attached to the document body.
|
||||
'<div class="md-datepicker-calendar-pane md-whiteframe-z1">' +
|
||||
'<div class="md-datepicker-input-mask"></div>' +
|
||||
'<div class="md-datepicker-input-mask">' +
|
||||
'<div class="md-datepicker-input-mask-opaque"></div>' +
|
||||
'</div>' +
|
||||
'<div class="md-datepicker-calendar">' +
|
||||
'<md-calendar role="dialog" aria-label="{{::ctrl.dateLocale.msgCalendar}}" ' +
|
||||
'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen"></md-calendar>' +
|
||||
@@ -6720,6 +6772,12 @@ function iosScrollFix(node) {
|
||||
/** @type {HTMLElement} Calendar icon button. */
|
||||
this.calendarButton = $element[0].querySelector('.md-datepicker-button');
|
||||
|
||||
/**
|
||||
* Element covering everything but the input in the top of the floating calendar pane.
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque');
|
||||
|
||||
/** @final {!angular.JQLite} */
|
||||
this.$element = $element;
|
||||
|
||||
@@ -6765,7 +6823,7 @@ function iosScrollFix(node) {
|
||||
|
||||
this.installPropertyInterceptors();
|
||||
this.attachChangeListeners();
|
||||
this.attachInterationListeners();
|
||||
this.attachInteractionListeners();
|
||||
|
||||
var self = this;
|
||||
$scope.$on('$destroy', function() {
|
||||
@@ -6799,18 +6857,22 @@ function iosScrollFix(node) {
|
||||
|
||||
self.$scope.$on('md-calendar-change', function(event, date) {
|
||||
self.ngModelCtrl.$setViewValue(date);
|
||||
self.date = date;
|
||||
self.inputElement.value = self.dateLocale.formatDate(date);
|
||||
self.closeCalendarPane();
|
||||
self.resizeInputElement();
|
||||
self.inputContainer.classList.remove(INVALID_CLASS);
|
||||
});
|
||||
|
||||
self.inputElement.addEventListener('input', angular.bind(self, self.resizeInputElement));
|
||||
var ngElement = angular.element(self.inputElement);
|
||||
ngElement.on('input', angular.bind(self, self.resizeInputElement));
|
||||
// TODO(chenmike): Add ability for users to specify this interval.
|
||||
self.inputElement.addEventListener('input', self.$mdUtil.debounce(self.handleInputEvent,
|
||||
ngElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
|
||||
DEFAULT_DEBOUNCE_INTERVAL, self));
|
||||
};
|
||||
|
||||
/** Attach event listeners for user interaction. */
|
||||
DatePickerCtrl.prototype.attachInterationListeners = function() {
|
||||
DatePickerCtrl.prototype.attachInteractionListeners = function() {
|
||||
var self = this;
|
||||
var $scope = this.$scope;
|
||||
var keyCodes = this.$mdConstant.KEY_CODE;
|
||||
@@ -6845,7 +6907,7 @@ function iosScrollFix(node) {
|
||||
|
||||
Object.defineProperty(this, 'placeholder', {
|
||||
get: function() { return self.inputElement.placeholder; },
|
||||
set: function(value) { self.inputElement.placeholder = value; }
|
||||
set: function(value) { self.inputElement.placeholder = value || ''; }
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6878,7 +6940,6 @@ function iosScrollFix(node) {
|
||||
this.ngModelCtrl.$setViewValue(parsedDate);
|
||||
this.date = parsedDate;
|
||||
this.inputContainer.classList.remove(INVALID_CLASS);
|
||||
this.$scope.$apply();
|
||||
} else {
|
||||
// If there's an input string, it's an invalid date.
|
||||
this.inputContainer.classList.toggle(INVALID_CLASS, inputString);
|
||||
@@ -6897,6 +6958,12 @@ function iosScrollFix(node) {
|
||||
calendarPane.style.top = (elementRect.top - bodyRect.top) + 'px';
|
||||
document.body.appendChild(this.calendarPane);
|
||||
|
||||
// The top of the calendar pane is a transparent box that shows the text input underneath.
|
||||
// Since the pane is flowing, though, the page underneath the pane *adjacent* to the input is
|
||||
// also shown unless we cover it up. The inputMask does this by filling up the remaining space
|
||||
// based on the width of the input.
|
||||
this.inputMask.style.left = elementRect.width + 'px';
|
||||
|
||||
// Add CSS class after one frame to trigger open animation.
|
||||
this.$$rAF(function() {
|
||||
calendarPane.classList.add('md-pane-open');
|
||||
@@ -6908,9 +6975,11 @@ function iosScrollFix(node) {
|
||||
this.$element.removeClass('md-datepicker-open');
|
||||
this.calendarPane.classList.remove('md-pane-open');
|
||||
|
||||
// Use native DOM removal because we do not want any of the angular state of this element
|
||||
// to be disposed.
|
||||
this.calendarPane.parentNode.removeChild(this.calendarPane);
|
||||
if (this.calendarPane.parentNode) {
|
||||
// Use native DOM removal because we do not want any of the angular state of this element
|
||||
// to be disposed.
|
||||
this.calendarPane.parentNode.removeChild(this.calendarPane);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -7182,7 +7251,7 @@ function iosScrollFix(node) {
|
||||
* @return {boolean} Whether the date is a valid Date.
|
||||
*/
|
||||
function isValidDate(date) {
|
||||
return date !== null && date.getTime && !isNaN(date.getTime());
|
||||
return date != null && date.getTime && !isNaN(date.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8746,7 +8815,7 @@ MdDividerDirective.$inject = ["$mdTheming"];
|
||||
'</div>',
|
||||
|
||||
scope: {
|
||||
direction: '=?mdDirection',
|
||||
direction: '@?mdDirection',
|
||||
isOpen: '=?mdOpen'
|
||||
},
|
||||
|
||||
@@ -10662,6 +10731,7 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
|
||||
function postLink(scope, element, attr, ctrls) {
|
||||
|
||||
var containerCtrl = ctrls[0];
|
||||
var hasNgModel = !!ctrls[1];
|
||||
var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
|
||||
var isReadonly = angular.isDefined(attr.readonly);
|
||||
|
||||
@@ -10684,6 +10754,13 @@ function inputTextareaDirective($mdUtil, $window, $mdAria) {
|
||||
setupTextarea();
|
||||
}
|
||||
|
||||
// If the input doesn't have an ngModel, it may have a static value. For that case,
|
||||
// we have to do one initial check to determine if the container should be in the
|
||||
// "has a value" state.
|
||||
if (!hasNgModel) {
|
||||
inputCheckValue();
|
||||
}
|
||||
|
||||
var isErrorGetter = containerCtrl.isErrorGetter || function() {
|
||||
return ngModelCtrl.$invalid && ngModelCtrl.$touched;
|
||||
};
|
||||
@@ -11311,7 +11388,7 @@ angular.module('material.components.menu', [
|
||||
*
|
||||
*/
|
||||
|
||||
function MenuDirective($mdMenu) {
|
||||
function MenuDirective() {
|
||||
var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
|
||||
return {
|
||||
restrict: 'E',
|
||||
@@ -11352,7 +11429,6 @@ function MenuDirective($mdMenu) {
|
||||
|
||||
}
|
||||
}
|
||||
MenuDirective.$inject = ["$mdMenu"];
|
||||
|
||||
function MenuController($mdMenu, $attrs, $element, $scope) {
|
||||
var menuContainer;
|
||||
@@ -13114,7 +13190,7 @@ function OptionDirective($mdButtonInkRipple, $mdUtil) {
|
||||
if (angular.isDefined(attr.ngValue)) {
|
||||
scope.$watch(attr.ngValue, setOptionValue);
|
||||
} else if (angular.isDefined(attr.value)) {
|
||||
setOptionValue(attr.value);
|
||||
setOptionValue(isNaN(attr.value) ? attr.value : Number(attr.value));
|
||||
} else {
|
||||
scope.$watch(function() {
|
||||
return element.text();
|
||||
@@ -13564,6 +13640,7 @@ function SelectProvider($$interimElementProvider) {
|
||||
|
||||
function checkCloseMenu(ev) {
|
||||
if (ev && ( ev.type == 'mouseup') && (ev.currentTarget != dropDown[0])) return;
|
||||
if ( mouseOnScrollbar() ) return;
|
||||
|
||||
if (!selectCtrl.isMultiple) {
|
||||
opts.restoreFocus = true;
|
||||
@@ -13572,6 +13649,23 @@ function SelectProvider($$interimElementProvider) {
|
||||
$mdSelect.hide(selectCtrl.ngModel.$viewValue);
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the mouseup event was on a scrollbar
|
||||
*/
|
||||
function mouseOnScrollbar() {
|
||||
var clickOnScrollbar = false;
|
||||
if(ev.currentTarget.children.length > 0) {
|
||||
var child = ev.currentTarget.children[0];
|
||||
var hasScrollbar = child.scrollHeight > child.clientHeight;
|
||||
if (hasScrollbar && child.children.length > 0) {
|
||||
var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left;
|
||||
if(relPosX > child.children[0].offsetWidth)
|
||||
clickOnScrollbar = true;
|
||||
}
|
||||
}
|
||||
return clickOnScrollbar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14945,46 +15039,57 @@ angular.module('material.components.subheader', [
|
||||
* </hljs>
|
||||
*/
|
||||
|
||||
function MdSubheaderDirective($mdSticky, $compile, $mdTheming) {
|
||||
function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template:
|
||||
'<h2 class="md-subheader">' +
|
||||
'<div class="md-subheader-inner">' +
|
||||
'<span class="md-subheader-content"></span>' +
|
||||
'</div>' +
|
||||
'</h2>',
|
||||
compile: function(element, attr, transclude) {
|
||||
return function postLink(scope, element, attr) {
|
||||
$mdTheming(element);
|
||||
var outerHTML = element[0].outerHTML;
|
||||
template: (
|
||||
'<h2 class="md-subheader">' +
|
||||
' <div class="md-subheader-inner">' +
|
||||
' <span class="md-subheader-content"></span>' +
|
||||
' </div>' +
|
||||
'</h2>'
|
||||
),
|
||||
link: function postLink(scope, element, attr, controllers, transclude) {
|
||||
$mdTheming(element);
|
||||
var outerHTML = element[0].outerHTML;
|
||||
|
||||
function getContent(el) {
|
||||
return angular.element(el[0].querySelector('.md-subheader-content'));
|
||||
}
|
||||
function getContent(el) {
|
||||
return angular.element(el[0].querySelector('.md-subheader-content'));
|
||||
}
|
||||
|
||||
// Transclude the user-given contents of the subheader
|
||||
// the conventional way.
|
||||
// Transclude the user-given contents of the subheader
|
||||
// the conventional way.
|
||||
transclude(scope, function(clone) {
|
||||
getContent(element).append(clone);
|
||||
});
|
||||
|
||||
// Create another clone, that uses the outer and inner contents
|
||||
// of the element, that will be 'stickied' as the user scrolls.
|
||||
if (!element.hasClass('md-no-sticky')) {
|
||||
transclude(scope, function(clone) {
|
||||
getContent(element).append(clone);
|
||||
});
|
||||
// If the user adds an ng-if or ng-repeat directly to the md-subheader element, the
|
||||
// compiled clone below will only be a comment tag (since they replace their elements with
|
||||
// a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own
|
||||
// DIV to ensure we have something $mdSticky can use
|
||||
var wrapperHtml = '<div class="md-subheader-wrapper">' + outerHTML + '</div>';
|
||||
var stickyClone = $compile(wrapperHtml)(scope);
|
||||
|
||||
// Create another clone, that uses the outer and inner contents
|
||||
// of the element, that will be 'stickied' as the user scrolls.
|
||||
if (!element.hasClass('md-no-sticky')) {
|
||||
transclude(scope, function(clone) {
|
||||
var stickyClone = $compile(angular.element(outerHTML))(scope);
|
||||
// Append the sticky
|
||||
$mdSticky(scope, element, stickyClone);
|
||||
|
||||
// Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before
|
||||
// attempting to create the clone
|
||||
$mdUtil.nextTick(function() {
|
||||
getContent(stickyClone).append(clone);
|
||||
$mdSticky(scope, element, stickyClone);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming"];
|
||||
MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil"];
|
||||
|
||||
})();
|
||||
(function(){
|
||||
@@ -15822,7 +15927,25 @@ function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdThe
|
||||
|
||||
function bindEvents () {
|
||||
var mouseActive = false;
|
||||
var enterHandler = function() {
|
||||
|
||||
var ngWindow = angular.element($window);
|
||||
|
||||
// Store whether the element was focused when the window loses focus.
|
||||
var windowBlurHandler = function() {
|
||||
elementFocusedOnWindowBlur = document.activeElement === parent[0];
|
||||
};
|
||||
var elementFocusedOnWindowBlur = false;
|
||||
ngWindow.on('blur', windowBlurHandler);
|
||||
scope.$on('$destroy', function() {
|
||||
ngWindow.off('blur', windowBlurHandler);
|
||||
});
|
||||
|
||||
var enterHandler = function(e) {
|
||||
// Prevent the tooltip from showing when the window is receiving focus.
|
||||
if (e.type === 'focus' && elementFocusedOnWindowBlur) {
|
||||
elementFocusedOnWindowBlur = false;
|
||||
return;
|
||||
}
|
||||
parent.on('blur mouseleave touchend touchcancel', leaveHandler );
|
||||
setVisible(true);
|
||||
};
|
||||
@@ -16271,6 +16394,15 @@ VirtualRepeatContainerController.prototype.handleScroll_ = function() {
|
||||
* @param {string=} md-extra-name Evaluates to an additional name to which
|
||||
* the current iterated item can be assigned on the repeated scope. (Needed
|
||||
* for use in md-autocomplete).
|
||||
* @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument
|
||||
* as an object that can fetch rows rather than an array.
|
||||
* NOTE: This object must implement the following interface with two (2) methods:
|
||||
* getItemAtIndex: function(index) -> item at that index or null if it is not yet
|
||||
* loaded (It should start downloading the item in that case).
|
||||
* getLength: function() -> number The data legnth to which the repeater container
|
||||
* should be sized. Ideally, when the count is known, this method should return it.
|
||||
* Otherwise, return a higher number than the currently loaded items to produce an
|
||||
* infinite-scroll behavior.
|
||||
*/
|
||||
function VirtualRepeatDirective($parse) {
|
||||
return {
|
||||
@@ -16305,12 +16437,16 @@ function VirtualRepeatController($scope, $element, $attrs, $browser, $document,
|
||||
this.$document = $document;
|
||||
this.$$rAF = $$rAF;
|
||||
|
||||
/** @type {boolean} Whether we are in on-demand mode. */
|
||||
this.onDemand = $attrs.hasOwnProperty('mdOnDemand');
|
||||
/** @type {!Function} Backup reference to $browser.$$checkUrlChange */
|
||||
this.browserCheckUrlChange = $browser.$$checkUrlChange;
|
||||
/** @type {number} Most recent starting repeat index (based on scroll offset) */
|
||||
this.newStartIndex = 0;
|
||||
/** @type {number} Most recent ending repeat index (based on scroll offset) */
|
||||
this.newEndIndex = 0;
|
||||
/** @type {number} Most recent end visible index (based on scroll offset) */
|
||||
this.newVisibleEnd = 0;
|
||||
/** @type {number} Previous starting repeat index (based on scroll offset) */
|
||||
this.startIndex = 0;
|
||||
/** @type {number} Previous ending repeat index (based on scroll offset) */
|
||||
@@ -16365,10 +16501,12 @@ VirtualRepeatController.prototype.link_ =
|
||||
this.container = container;
|
||||
this.transclude = transclude;
|
||||
this.repeatName = repeatName;
|
||||
this.repeatListExpression = repeatListExpression;
|
||||
this.rawRepeatListExpression = repeatListExpression;
|
||||
this.extraName = extraName;
|
||||
this.sized = false;
|
||||
|
||||
this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
|
||||
|
||||
this.container.register(this);
|
||||
};
|
||||
|
||||
@@ -16396,6 +16534,25 @@ VirtualRepeatController.prototype.readItemSize_ = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the user-specified repeat list, transforming it into an array-like
|
||||
* object in the case of infinite scroll/dynamic load mode.
|
||||
* @param {!angular.Scope} The scope.
|
||||
* @return {!Array|!Object} An array or array-like object for iteration.
|
||||
*/
|
||||
VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
|
||||
var repeatList = this.rawRepeatListExpression(scope);
|
||||
|
||||
if (this.onDemand && repeatList) {
|
||||
var virtualList = new VirtualRepeatModelArrayLike(repeatList);
|
||||
virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
|
||||
return virtualList;
|
||||
} else {
|
||||
return repeatList;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called by the container. Informs us that the containers scroll or size has
|
||||
* changed.
|
||||
@@ -16429,6 +16586,9 @@ VirtualRepeatController.prototype.containerUpdated = function() {
|
||||
if (this.newStartIndex !== this.startIndex ||
|
||||
this.newEndIndex !== this.endIndex ||
|
||||
this.container.getScrollOffset() > this.container.getScrollSize()) {
|
||||
if (this.items instanceof VirtualRepeatModelArrayLike) {
|
||||
this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
|
||||
}
|
||||
this.virtualRepeatUpdate_(this.items, this.items);
|
||||
}
|
||||
};
|
||||
@@ -16465,7 +16625,7 @@ VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItem
|
||||
}
|
||||
|
||||
this.items = items;
|
||||
if (items !== oldItems) {
|
||||
if (items !== oldItems || lengthChanged) {
|
||||
this.updateIndexes_();
|
||||
}
|
||||
|
||||
@@ -16646,10 +16806,59 @@ VirtualRepeatController.prototype.updateIndexes_ = function() {
|
||||
this.newStartIndex = Math.max(0, Math.min(
|
||||
itemsLength - containerLength,
|
||||
Math.floor(this.container.getScrollOffset() / this.itemSize)));
|
||||
this.newEndIndex = Math.min(itemsLength, this.newStartIndex + containerLength + NUM_EXTRA);
|
||||
this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
|
||||
this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
|
||||
this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
|
||||
};
|
||||
|
||||
/**
|
||||
* This VirtualRepeatModelArrayLike class enforces the interface requirements
|
||||
* for infinite scrolling within a mdVirtualRepeatContainer. An object with this
|
||||
* interface must implement the following interface with two (2) methods:
|
||||
*
|
||||
* getItemAtIndex: function(index) -> item at that index or null if it is not yet
|
||||
* loaded (It should start downloading the item in that case).
|
||||
*
|
||||
* getLength: function() -> number The data legnth to which the repeater container
|
||||
* should be sized. Ideally, when the count is known, this method should return it.
|
||||
* Otherwise, return a higher number than the currently loaded items to produce an
|
||||
* infinite-scroll behavior.
|
||||
*
|
||||
* @usage
|
||||
* <hljs lang="html">
|
||||
* <md-virtual-repeat-container md-orient-horizontal>
|
||||
* <div md-virtual-repeat="i in items" md-on-demand>
|
||||
* Hello {{i}}!
|
||||
* </div>
|
||||
* </md-virtual-repeat-container>
|
||||
* </hljs>
|
||||
*
|
||||
*/
|
||||
function VirtualRepeatModelArrayLike(model) {
|
||||
if (!angular.isFunction(model.getItemAtIndex) ||
|
||||
!angular.isFunction(model.getLength)) {
|
||||
throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' +
|
||||
'functions getItemAtIndex() and getLength() ');
|
||||
}
|
||||
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
|
||||
VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
|
||||
for (var i = start; i < end; i++) {
|
||||
if (!this.hasOwnProperty(i)) {
|
||||
this[i] = this.model.getItemAtIndex(i);
|
||||
}
|
||||
}
|
||||
this.length = this.model.getLength();
|
||||
};
|
||||
|
||||
|
||||
function abstractMethod() {
|
||||
throw Error('Non-overridden abstract method called.');
|
||||
}
|
||||
|
||||
})();
|
||||
(function(){
|
||||
"use strict";
|
||||
@@ -17857,6 +18066,7 @@ function MdChipTransclude ($compile) {
|
||||
newScope = ctrl.parent.$new(false, ctrl.parent);
|
||||
newScope.$$replacedScope = scope;
|
||||
newScope.$chip = scope.$chip;
|
||||
newScope.$index = scope.$index;
|
||||
newScope.$mdChipsCtrl = ctrl;
|
||||
|
||||
var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
|
||||
@@ -19906,7 +20116,7 @@ MdTabsTemplate.$inject = ["$compile", "$mdUtil"];
|
||||
|
||||
})();
|
||||
(function(){
|
||||
angular.module("material.core").constant("$MD_THEME_CSS", "/* mixin definition ; sets LTR and RTL within the same style call *//** * Position a FAB button. */md-autocomplete.md-THEME_NAME-theme { background: '{{background-50}}'; } md-autocomplete.md-THEME_NAME-theme[disabled] { background: '{{background-100}}'; } md-autocomplete.md-THEME_NAME-theme button md-icon path { fill: '{{background-600}}'; } md-autocomplete.md-THEME_NAME-theme button:after { background: '{{background-600-0.3}}'; }.md-autocomplete-suggestions.md-THEME_NAME-theme { background: '{{background-50}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li { color: '{{background-900}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li .highlight { color: '{{background-600}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li:hover, .md-autocomplete-suggestions.md-THEME_NAME-theme li.selected { background: '{{background-200}}'; }md-backdrop { background-color: '{{background-900-0.0}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme { background-color: '{{background-900-0.48}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme.ng-enter { background-color: '{{background-900-0.0}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme.ng-enter.md-opaque.md-THEME_NAME-theme.ng-enter-active { background-color: '{{background-900-0.48}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme.ng-leave.md-opaque.md-THEME_NAME-theme.ng-leave-active { background-color: '{{background-900-0.0}}'; }md-bottom-sheet.md-THEME_NAME-theme { background-color: '{{background-50}}'; border-top-color: '{{background-300}}'; } md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item { color: '{{foreground-1}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { background-color: '{{background-50}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { color: '{{foreground-1}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]):hover, .md-button.md-THEME_NAME-theme:not([disabled]):hover { background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme:not([disabled]).md-focused { background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover, .md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover { background-color: transparent; }a.md-button.md-THEME_NAME-theme.md-fab, .md-button.md-THEME_NAME-theme.md-fab { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab md-icon, .md-button.md-THEME_NAME-theme.md-fab md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; }a.md-button.md-THEME_NAME-theme.md-primary, .md-button.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised, a.md-button.md-THEME_NAME-theme.md-primary.md-fab, .md-button.md-THEME_NAME-theme.md-primary.md-raised, .md-button.md-THEME_NAME-theme.md-primary.md-fab { color: '{{primary-contrast}}'; background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon { color: '{{primary-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover { background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused { background-color: '{{primary-600}}'; } a.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon { color: '{{primary-color}}'; }a.md-button.md-THEME_NAME-theme.md-fab, .md-button.md-THEME_NAME-theme.md-fab { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEMLine truncated
|
||||
angular.module("material.core").constant("$MD_THEME_CSS", "md-autocomplete.md-THEME_NAME-theme { background: '{{background-50}}'; } md-autocomplete.md-THEME_NAME-theme[disabled] { background: '{{background-100}}'; } md-autocomplete.md-THEME_NAME-theme button md-icon path { fill: '{{background-600}}'; } md-autocomplete.md-THEME_NAME-theme button:after { background: '{{background-600-0.3}}'; }.md-autocomplete-suggestions.md-THEME_NAME-theme { background: '{{background-50}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li { color: '{{background-900}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li .highlight { color: '{{background-600}}'; } .md-autocomplete-suggestions.md-THEME_NAME-theme li:hover, .md-autocomplete-suggestions.md-THEME_NAME-theme li.selected { background: '{{background-200}}'; }md-backdrop { background-color: '{{background-900-0.0}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme { background-color: '{{background-900-0.48}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme.ng-enter { background-color: '{{background-900-0.0}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme.ng-enter.md-opaque.md-THEME_NAME-theme.ng-enter-active { background-color: '{{background-900-0.48}}'; } md-backdrop.md-opaque.md-THEME_NAME-theme.ng-leave.md-opaque.md-THEME_NAME-theme.ng-leave-active { background-color: '{{background-900-0.0}}'; }md-bottom-sheet.md-THEME_NAME-theme { background-color: '{{background-50}}'; border-top-color: '{{background-300}}'; } md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item { color: '{{foreground-1}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { background-color: '{{background-50}}'; } md-bottom-sheet.md-THEME_NAME-theme .md-subheader { color: '{{foreground-1}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]):hover, .md-button.md-THEME_NAME-theme:not([disabled]):hover { background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme:not([disabled]).md-focused { background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover, .md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover { background-color: transparent; }a.md-button.md-THEME_NAME-theme.md-fab, .md-button.md-THEME_NAME-theme.md-fab { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab md-icon, .md-button.md-THEME_NAME-theme.md-fab md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover { background-color: '{{accent-color}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused { background-color: '{{accent-A700}}'; }a.md-button.md-THEME_NAME-theme.md-primary, .md-button.md-THEME_NAME-theme.md-primary { color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised, a.md-button.md-THEME_NAME-theme.md-primary.md-fab, .md-button.md-THEME_NAME-theme.md-primary.md-raised, .md-button.md-THEME_NAME-theme.md-primary.md-fab { color: '{{primary-contrast}}'; background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon { color: '{{primary-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover { background-color: '{{primary-color}}'; } a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused { background-color: '{{primary-600}}'; } a.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon, .md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon { color: '{{primary-color}}'; }a.md-button.md-THEME_NAME-theme.md-fab, .md-button.md-THEME_NAME-theme.md-fab { background-color: '{{accent-color}}'; color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon { color: '{{accent-contrast}}'; } a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover, .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hoLine truncated
|
||||
})();
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user