VulpesOS/gaia/apps/camera/bower_components/gaia-header/dist/gaia-header-es5.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

1527 lines
55 KiB
JavaScript
Executable file

"use strict";
(function (f) {
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f();
} else if (typeof define === "function" && define.amd) {
define([], f);
} else {
var g;if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
g = this;
}g.GaiaHeader = f();
}
})(function () {
var define, module, exports;return (function e(t, n, r) {
function s(o, u) {
if (!n[o]) {
if (!t[o]) {
var a = typeof require == "function" && require;if (!u && a) return a(o, !0);if (i) return i(o, !0);var f = new Error("Cannot find module '" + o + "'");throw (f.code = "MODULE_NOT_FOUND", f);
}var l = n[o] = { exports: {} };t[o][0].call(l.exports, function (e) {
var n = t[o][1][e];return s(n ? n : e);
}, l, l.exports, e, t, n, r);
}return n[o].exports;
}var i = typeof require == "function" && require;for (var o = 0; o < r.length; o++) s(r[o]);return s;
})({ 1: [function (require, module, exports) {
;(function (define) {
define(function (require, exports, module) {
'use strict';
/**
* Simple logger.
*
* @return {Function}
*/
var debug = 0 ? console.log.bind(console) : function () {};
/**
* Global canvas cache.
*
* @type {Object}
*/
var cache = {};
/**
* Default min/max font-size.
*
* @type {Number}
*/
var MIN = 16;
var MAX = 24;
/**
* The number of pixels to subtract from
* the given `config.space` to ensure
* HTML text doesn't overflow container.
*
* Ideally we would use 1px, but in some
* cases italicised text in canvas is ~2px
* longer than the same text in HTML.
*
* http://bugzil.la/1126391
*
* @type {Number}
*/
var BUFFER = 3;
/**
* Get the font-size that closest fits
* the given space with the given font.
*
* Config:
*
* - {String} `text` The text string
* - {String} `font` Font shorthand string
* - {Number} `space` Width (px) to fit the text into
* - {Number} `min` Min font-size (px) (optional)
* - {Number} `max` Max font-size (px) (optional)
*
* @param {Object} config
* @return {Object} {fontSize,overflowing,textWidth}
*/
module.exports = function (config) {
debug('font fit', config);
var space = config.space - BUFFER;
var min = config.min || MIN;
var max = config.max || MAX;
var text = trim(config.text);
var fontSize = max;
var textWidth;
var font;
do {
font = config.font.replace(/\d+px/, fontSize + 'px');
textWidth = getTextWidth(text, font);
} while (textWidth > space && fontSize !== min && fontSize--);
return {
textWidth: textWidth,
fontSize: fontSize,
overflowing: textWidth > space
};
};
/**
* Get the width of the given text
* with the given font style.
*
* @param {String} text
* @param {String} font (CSS shorthand)
* @return {Number} (px)
*/
function getTextWidth(text, font) {
var ctx = getCanvasContext(font);
var width = ctx.measureText(text).width;
debug('got text width', width);
return width;
}
/**
* Get a canvas context configured
* to the given font style.
*
* @param {String} font
* @return {CanvasRenderingContext2D}
*/
function getCanvasContext(font) {
debug('get canvas context', font);
var cached = cache[font];
if (cached) {
return cached;
}
var canvas = document.createElement('canvas');
canvas.setAttribute('moz-opaque', 'true');
canvas.setAttribute('width', '1px');
canvas.setAttribute('height', '1px');
debug('created canvas', canvas);
var ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.font = font;
return cache[font] = ctx;
}
/**
* Trim leading, trailing
* and excess whitespace.
*
* @param {String} text
* @return {String}
*/
function trim(text) {
return text.replace(/\s+/g, ' ').trim();
}
});
})(typeof define == 'function' && define.amd ? define : (function (n, w) {
'use strict';return typeof module == 'object' ? function (c) {
c(require, exports, module);
} : function (c) {
var m = { exports: {} };c(function (n) {
return w[n];
}, m.exports, m);w[n] = m.exports;
};
})('font-fit', this));
}, {}], 2: [function (require, module, exports) {
/* globals define */
;(function (define) {
'use strict';define(function (require, exports, module) {
/**
* Locals
*/
var textContent = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
var innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
var removeAttribute = Element.prototype.removeAttribute;
var setAttribute = Element.prototype.setAttribute;
var noop = function noop() {};
/**
* Register a new component.
*
* @param {String} name
* @param {Object} props
* @return {constructor}
* @public
*/
exports.register = function (name, props) {
var baseProto = getBaseProto(props["extends"]);
var template = props.template || baseProto.templateString;
// Components are extensible by default but can be declared
// as non extensible as an optimization to avoid
// storing the template strings
var extensible = props.extensible = props.hasOwnProperty('extensible') ? props.extensible : true;
// Clean up
delete props["extends"];
// Pull out CSS that needs to be in the light-dom
if (template) {
// Stores the string to be reprocessed when
// a new component extends this one
if (extensible && props.template) {
props.templateString = props.template;
}
var output = processCss(template, name);
props.template = document.createElement('template');
props.template.innerHTML = output.template;
props.lightCss = output.lightCss;
props.globalCss = props.globalCss || '';
props.globalCss += output.globalCss;
}
// Inject global CSS into the document,
// and delete as no longer needed
injectGlobalCss(props.globalCss);
delete props.globalCss;
// Merge base getter/setter attributes with the user's,
// then define the property descriptors on the prototype.
var descriptors = mixin(props.attrs || {}, base.descriptors);
// Store the orginal descriptors somewhere
// a little more private and delete the original
props._attrs = props.attrs;
delete props.attrs;
// Create the prototype, extended from base and
// define the descriptors directly on the prototype
var proto = createProto(baseProto, props);
Object.defineProperties(proto, descriptors);
// Register the custom-element and return the constructor
try {
return document.registerElement(name, { prototype: proto });
} catch (e) {
if (e.name !== 'NotSupportedError') {
throw e;
}
}
};
var base = {
properties: {
GaiaComponent: true,
attributeChanged: noop,
attached: noop,
detached: noop,
created: noop,
createdCallback: function createdCallback() {
if (this.dirObserver) {
addDirObserver();
}
injectLightCss(this);
this.created();
},
/**
* It is very common to want to keep object
* properties in-sync with attributes,
* for example:
*
* el.value = 'foo';
* el.setAttribute('value', 'foo');
*
* So we support an object on the prototype
* named 'attrs' to provide a consistent
* way for component authors to define
* these properties. When an attribute
* changes we keep the attr[name]
* up-to-date.
*
* @param {String} name
* @param {String||null} from
* @param {String||null} to
*/
attributeChangedCallback: function attributeChangedCallback(name, from, to) {
var prop = toCamelCase(name);
if (this._attrs && this._attrs[prop]) {
this[prop] = to;
}
this.attributeChanged(name, from, to);
},
attachedCallback: function attachedCallback() {
if (this.dirObserver) {
this.setInnerDirAttributes = setInnerDirAttributes.bind(null, this);
document.addEventListener('dirchanged', this.setInnerDirAttributes);
}
this.attached();
},
detachedCallback: function detachedCallback() {
if (this.dirObserver) {
document.removeEventListener('dirchanged', this.setInnerDirAttributes);
}
this.detached();
},
/**
* A convenient method for setting up
* a shadow-root using the defined template.
*
* @return {ShadowRoot}
*/
setupShadowRoot: function setupShadowRoot() {
if (!this.template) {
return;
}
var node = document.importNode(this.template.content, true);
this.createShadowRoot().appendChild(node);
if (this.dirObserver) {
setInnerDirAttributes(this);
}
return this.shadowRoot;
},
/**
* Sets an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
setAttr: function setAttr(name, value) {
var internal = this.shadowRoot.firstElementChild;
setAttribute.call(internal, name, value);
setAttribute.call(this, name, value);
},
/**
* Removes an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
removeAttr: function removeAttr(name) {
var internal = this.shadowRoot.firstElementChild;
removeAttribute.call(internal, name);
removeAttribute.call(this, name);
}
},
descriptors: {
textContent: {
set: function set(value) {
textContent.set.call(this, value);
if (this.lightStyle) {
this.appendChild(this.lightStyle);
}
},
get: function get() {
return textContent.get();
}
},
innerHTML: {
set: function set(value) {
innerHTML.set.call(this, value);
if (this.lightStyle) {
this.appendChild(this.lightStyle);
}
},
get: innerHTML.get
}
}
};
/**
* The default base prototype to use
* when `extends` is undefined.
*
* @type {Object}
*/
var defaultPrototype = createProto(HTMLElement.prototype, base.properties);
/**
* Returns a suitable prototype based
* on the object passed.
*
* @private
* @param {HTMLElementPrototype|undefined} proto
* @return {HTMLElementPrototype}
*/
function getBaseProto(proto) {
if (!proto) {
return defaultPrototype;
}
proto = proto.prototype || proto;
return !proto.GaiaComponent ? createProto(proto, base.properties) : proto;
}
/**
* Extends the given proto and mixes
* in the given properties.
*
* @private
* @param {Object} proto
* @param {Object} props
* @return {Object}
*/
function createProto(proto, props) {
return mixin(Object.create(proto), props);
}
/**
* Detects presence of shadow-dom
* CSS selectors.
*
* @private
* @return {Boolean}
*/
var hasShadowCSS = (function () {
var div = document.createElement('div');
try {
div.querySelector(':host');div.querySelector('::content');return true;
} catch (e) {
return false;
}
})();
/**
* Regexs used to extract shadow-css
*
* @type {Object}
*/
var regex = {
shadowCss: /(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,
':host': /(?:\:host)/g,
':host()': /\:host\((.+)\)(?: \:\:content)?/g,
':host-context': /\:host-context\((.+)\)([^{,]+)?/g,
'::content': /(?:\:\:content)/g
};
/**
* Extracts the :host and ::content rules
* from the shadow-dom CSS and rewrites
* them to work from the <style scoped>
* injected at the root of the component.
*
* @private
* @return {String}
*/
function processCss(template, name) {
var globalCss = '';
var lightCss = '';
if (!hasShadowCSS) {
template = template.replace(regex.shadowCss, function (match) {
var hostContext = regex[':host-context'].exec(match);
if (hostContext) {
globalCss += match.replace(regex['::content'], '').replace(regex[':host-context'], '$1 ' + name + '$2').replace(/ +/g, ' '); // excess whitespace
} else {
lightCss += match.replace(regex[':host()'], name + '$1').replace(regex[':host'], name).replace(regex['::content'], name);
}
return '';
});
}
return {
template: template,
lightCss: lightCss,
globalCss: globalCss
};
}
/**
* Some CSS rules, such as @keyframes
* and @font-face don't work inside
* scoped or shadow <style>. So we
* have to put them into 'global'
* <style> in the head of the
* document.
*
* @private
* @param {String} css
*/
function injectGlobalCss(css) {
if (!css) {
return;
}
var style = document.createElement('style');
style.innerHTML = css.trim();
headReady().then(function () {
document.head.appendChild(style);
});
}
/**
* Resolves a promise once document.head is ready.
*
* @private
*/
function headReady() {
return new Promise(function (resolve) {
if (document.head) {
return resolve();
}
window.addEventListener('load', function fn() {
window.removeEventListener('load', fn);
resolve();
});
});
}
/**
* The Gecko platform doesn't yet have
* `::content` or `:host`, selectors,
* without these we are unable to style
* user-content in the light-dom from
* within our shadow-dom style-sheet.
*
* To workaround this, we clone the <style>
* node into the root of the component,
* so our selectors are able to target
* light-dom content.
*
* @private
*/
function injectLightCss(el) {
if (hasShadowCSS) {
return;
}
var stylesheet = el.querySelector('style');
if (!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.setAttribute('scoped', '');
stylesheet.appendChild(document.createTextNode(el.lightCss));
el.appendChild(stylesheet);
}
el.lightStyle = stylesheet;
}
/**
* Convert hyphen separated
* string to camel-case.
*
* Example:
*
* toCamelCase('foo-bar'); //=> 'fooBar'
*
* @private
* @param {String} string
* @return {String}
*/
function toCamelCase(string) {
return string.replace(/-(.)/g, function replacer(string, p1) {
return p1.toUpperCase();
});
}
/**
* Observer (singleton)
*
* @type {MutationObserver|undefined}
*/
var dirObserver;
/**
* Workaround for bug 1100912: applies a `dir` attribute to all shadowRoot
* children so that :-moz-dir() selectors work on shadow DOM elements.
*
* In order to keep decent performances, the `dir` is the component dir if
* defined, or the document dir otherwise. This won't work if the component's
* direction is defined by CSS or inherited from a parent container.
*
* This method should be removed when bug 1100912 is fixed.
*
* @private
* @param {WebComponent}
*/
function setInnerDirAttributes(component) {
var dir = component.dir || document.dir;
Array.from(component.shadowRoot.children).forEach(function (element) {
if (element.nodeName !== 'STYLE') {
element.dir = dir;
}
});
}
/**
* Observes the document `dir` (direction) attribute and when it changes:
* - dispatches a global `dirchanged` event;
* - forces the `dir` attribute of all shadowRoot children.
*
* Components can listen to this event and make internal changes if needed.
*
* @private
*/
function addDirObserver() {
if (dirObserver) {
return;
}
dirObserver = new MutationObserver(onChanged);
dirObserver.observe(document.documentElement, {
attributeFilter: ['dir'],
attributes: true
});
function onChanged(mutations) {
document.dispatchEvent(new Event('dirchanged'));
}
}
/**
* Copy the values of all properties from
* source object `target` to a target object `source`.
* It will return the target object.
*
* @private
* @param {Object} target
* @param {Object} source
* @returns {Object}
*/
function mixin(target, source) {
for (var key in source) {
target[key] = source[key];
}
return target;
}
});
})(typeof define == 'function' && define.amd ? define : (function (n, w) {
'use strict';return typeof module == 'object' ? function (c) {
c(require, exports, module);
} : function (c) {
var m = { exports: {} };c(function (n) {
return w[n];
}, m.exports, m);w[n] = m.exports;
};
})('gaia-component', this));
}, {}], 3: [function (require, module, exports) {
/* global define */
(function (define) {
'use strict';define(function (require, exports, module) {
/**
* Exports
*/
var base = window.GAIA_ICONS_BASE_URL || window.COMPONENTS_BASE_URL || 'bower_components/';
if (!document.documentElement) {
window.addEventListener('load', load);
} else {
load();
}
function load() {
if (isLoaded()) {
return;
}
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = base + 'gaia-icons/gaia-icons.css';
document.head.appendChild(link);
exports.loaded = true;
}
function isLoaded() {
return exports.loaded || document.querySelector('link[href*=gaia-icons]') || document.documentElement.classList.contains('gaia-icons-loaded');
}
});
})(typeof define == 'function' && define.amd ? define : (function (n, w) {
'use strict';return typeof module == 'object' ? function (c) {
c(require, exports, module);
} : function (c) {
var m = { exports: {} };c(function (n) {
return w[n];
}, m.exports, m);w[n] = m.exports;
};
})('gaia-icons', this));
}, {}], 4: [function (require, module, exports) {
/* globals define */
;(function (define) {
'use strict';define(function (require, exports, module) {
/**
* Dependencies
*/
var component = require('gaia-component');
var _fontFit = require('font-fit');
/**
* Load 'gaia-icons' font-family
*/
require('gaia-icons');
/**
* Simple logger (toggle 0)
*
* @type {Function}
*/
var debug = 0 ? console.log.bind(console) : function () {};
/**
* Supported action types
*
* @type {Object}
*/
var KNOWN_ACTIONS = {
menu: 'menu',
back: 'back',
close: 'close'
};
/**
* The default title font.
*
* @type {String}
*/
var TITLE_FONT = 'italic 300 24px FiraSans';
/**
* The padding (start/end) used if
* the title needs padding away from
* the edge of the header.
*
* @type {Number}
*/
var TITLE_PADDING = 10;
/**
* This is the minimum font size that we can take
* when the header title is centered in the window.
*/
var MINIMUM_FONT_SIZE_CENTERED = 20;
/**
* This is the minimum font size that we can take
* when the header title is not centered in the window.
*/
var MINIMUM_FONT_SIZE_UNCENTERED = 16;
/**
* This is the maximum font-size
* for the header title.
*/
var MAXIMUM_FONT_SIZE = 23;
/**
* Register the element.
*
* @return {Element} constructor
*/
module.exports = component.register('gaia-header', {
extensible: false, // discards some strings
dirObserver: true, // triggers a workaround for bug 1100912 in gaia-component
/**
* Called when the element is first created.
*
* Here we create the shadow-root and
* inject our template into it.
*
* @private
*/
created: function created() {
var _this = this;
debug('created');
this.setupShadowRoot();
// Elements
this.els = {
actionButton: this.shadowRoot.querySelector('.action-button'),
titles: this.getElementsByTagName('h1')
};
// Events
this.els.actionButton.addEventListener('click', function (e) {
return _this.onActionButtonClick(e);
});
this.observer = new MutationObserver(this.onMutation.bind(this));
// Properties
this.ignoreDir = this.hasAttribute('ignore-dir');
this.titleEnd = this.getAttribute('title-end');
this.titleStart = this.getAttribute('title-start');
this.noFontFit = this.getAttribute('no-font-fit');
this.notFlush = this.hasAttribute('not-flush');
this.action = this.getAttribute('action');
this.unresolved = {};
this.pending = {};
this._resizeThrottlingId = null;
// bind the listener in advance so that we can remove it when detaching.
this.onResize = this.onResize.bind(this);
},
/**
* Called when the element is
* attached to the DOM.
*
* Run fontFit when we have DOM
* context and start listening
* for DOM mutations.
*
* We run font-fit on next tick to
* avoid reading from the DOM when
* it may not be loaded yet.
*
* @private
*/
attached: function attached() {
debug('attached');
this.runFontFitSoon();
this.observerStart();
window.addEventListener('resize', this.onResize);
},
/**
* Called when the element is
* detached from the DOM.
*
* @private
*/
detached: function detached() {
debug('detached');
window.removeEventListener('resize', this.onResize);
this.observerStop();
this.clearPending();
},
/**
* Clears pending `.nextTick()`s and requestAnimationFrame's.
*
* @private
*/
clearPending: function clearPending() {
for (var key in this.pending) {
this.pending[key].clear();
delete this.pending[key];
}
window.cancelAnimationFrame(this._resizeThrottlingId);
this._resizeThrottlingId = null;
},
/**
* Run the font-fit logic and
* center the title.
*
* The styles are calculated synchronously,
* but then set asynchronously. This means
* this function can be hammered in one turn,
* and the title styles will only be written
* once, with the latest styles.
*
* @return {Promise}
* @public
*/
runFontFit: function runFontFit() {
var _this2 = this;
debug('run font-fit');
// Nothing is run if `no-font-fit` attribute
// is present. We don't `reject()` as this
// isn't technically an error state.
if (this.noFontFit) {
return Promise.resolve();
}
var titles = this.els.titles;
var space = this.getTitleSpace();
var styles = [].map.call(titles, function (el) {
return _this2.getTitleStyle(el, space);
});
// Update the title styles using the latest
// styles. This function can be called many
// times but will only run once in each tick.
return this.setTitleStylesSoon(styles);
},
/**
* Microtask debounced `runFontFit`
*
* @private
*/
runFontFitSoon: function runFontFitSoon() {
var _this3 = this;
debug('run font-fit soon');
if (this.pending.runFontFitSoon) {
return;
}
this.pending.runFontFitSoon = this.nextTick(function () {
delete _this3.pending.runFontFitSoon;
_this3.runFontFit();
});
},
/**
* Get the style properties required
* to fit and position the title text.
*
* @param {HTMLH1Element} el
* @param {Number} space
* @return {Object} {id, fontSize, marginStart, overflowing, padding}
* @private
*/
getTitleStyle: function getTitleStyle(el, space) {
debug('get el style', el, space);
var text = el.textContent;
var styleId = space.start + text + space.end + '#' + space.value;
// Bail when there's no text (or just whitespace)
if (!text || !text.trim()) {
return debug('exit: no text');
}
// If neither the text or the titleSpace
// changed, there's no reason to continue.
if (getStyleId(el) === styleId) {
return debug('exit: no change');
}
var marginStart = this.getTitleMarginStart();
var textSpace = space.value - Math.abs(marginStart);
var fontFitResult = this.fontFit(text, textSpace, {
min: MINIMUM_FONT_SIZE_CENTERED
});
var overflowing = fontFitResult.overflowing;
var padding = { start: 0, end: 0 };
// If the text is overflowing in the
// centered title, we remove centering
// to free up space, rerun fontFit to
// get a fontSize which fits this space.
if (overflowing) {
debug('title overflowing');
padding.start = !space.start ? TITLE_PADDING : 0;
padding.end = !space.end ? TITLE_PADDING : 0;
textSpace = space.value - padding.start - padding.end;
fontFitResult = this.fontFit(text, textSpace);
marginStart = 0;
}
return {
id: styleId,
fontSize: fontFitResult.fontSize,
marginStart: marginStart,
overflowing: overflowing,
padding: padding
};
},
/**
* Sets styles on the title elements.
*
* If there is already an unresolved Promise
* we return it instead of scheduling another.
* This means that the function can be hammered
* in the same sync-turn and will only run
* once (like a debounce).
*
* @param {Array} styles
* @return {Promise}
*/
setTitleStylesSoon: function setTitleStylesSoon(styles) {
var _this4 = this;
debug('set title styles soon', styles);
var key = 'setStyleTitlesSoon';
// Always update styles
this._titleStyles = styles;
// Return existing unresolved
// promise, or make a new one
if (this.unresolved[key]) {
return this.unresolved[key];
}
this.unresolved[key] = new Promise(function (resolve) {
_this4.pending[key] = _this4.nextTick(function () {
var styles = _this4._titleStyles;
var els = _this4.els.titles;
[].forEach.call(els, function (el, i) {
if (!styles[i]) {
return debug('exit');
}
_this4.setTitleStyle(el, styles[i]);
});
// Clean up
delete _this4._titleStyles;
delete _this4.unresolved[key];
delete _this4.pending[key];
resolve();
});
});
},
/**
* Fit text and center a title between the buttons before and after.
*
* @param {HTMLH1Element} el
* @param {Object} style
* @private
*/
setTitleStyle: function setTitleStyle(el, style) {
debug('set title style', style);
this.observerStop();
if (this.ignoreDir) {
el.style.marginLeft = style.marginStart + 'px';
el.style.paddingLeft = style.padding.start + 'px';
el.style.paddingRight = style.padding.end + 'px';
} else {
el.style.marginInlineStart = style.marginStart + 'px';
el.style.paddingInlineStart = style.padding.start + 'px';
el.style.paddingInlineEnd = style.padding.end + 'px';
}
el.style.fontSize = style.fontSize + 'px';
setStyleId(el, style.id);
this.observerStart();
},
/**
* Run font-fit on a title with
* the given amount of content space.
*
* @param {String} text
* @param {Number} space
* @param {Object} optional {[min]}
* @return {Object} {fontSize, textWidth}
* @private
*/
fontFit: function fontFit(text, space) {
var opts = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
debug('font fit:', text, space, opts);
var fontFitArgs = {
font: TITLE_FONT,
min: opts.min || MINIMUM_FONT_SIZE_UNCENTERED,
max: MAXIMUM_FONT_SIZE,
text: text,
space: space
};
return _fontFit(fontFitArgs);
},
/**
* Start the observer listening
* for DOM mutations.
* Start the listener for 'resize' event.
*
* @private
*/
observerStart: function observerStart() {
if (this.observing) {
return;
}
this.observer.observe(this, {
childList: true,
attributes: true,
subtree: true
});
this.observing = true;
debug('observer started');
},
/**
* Stop the observer listening
* for DOM mutations.
*
* @private
*/
observerStop: function observerStop() {
if (!this.observing) {
return;
}
this.observer.disconnect();
this.observing = false;
debug('observer stopped');
},
/**
* Handle 'resize' events.
* @param {Event} The DOM Event that's being handled.
*
* @private
*/
onResize: function onResize(e) {
var _this5 = this;
debug('onResize', this._resizeThrottlingId);
if (this._resizeThrottlingId !== null) {
return;
}
/* Resize events can arrive at a very high rate, so we're trying to
* reasonably throttle these events. */
this._resizeThrottlingId = window.requestAnimationFrame(function () {
_this5._resizeThrottlingId = null;
_this5.runFontFitSoon();
});
},
/**
* When the components DOM changes we
* call `.runFontFit()` (sync).
*
* If the `textContent` is changed in a
* mutation observer just after attaching,
* we end up running twice.
*
* If there is a pending async .runFontFit(),
* then we don't want to run it now.
*
* @param {Array} mutations
* @private
*/
onMutation: function onMutation(mutations) {
debug('on mutation', mutations);
if (!this.pending.runFontFitSoon) {
this.runFontFit();
}
},
/**
* Get the title width.
*
* Returns the space available for <h1>.
*
* @return {Number}
* @private
*/
getTitleSpace: function getTitleSpace() {
var start = this.titleStart;
var end = this.titleEnd;
var space = this.getWidth() - start - end;
var result = {
value: space,
start: start,
end: end
};
debug('get title space', result);
return result;
},
/**
* Get the width of the component.
*
* For performance reasons we make the
* assumption that the width is the same
* as `window.innerWidth` unless the
* `not-flush` attribute is used.
*
* @return {Number}
* @private
*/
getWidth: function getWidth() {
var value = this.notFlush ? this.clientWidth : window.innerWidth;
debug('get width', value);
return value;
},
/**
* Triggers the 'action' button
* (used in Gaia integration testing).
*
* @public
*/
triggerAction: function triggerAction() {
if (this.action) {
this.els.actionButton.click();
}
},
/**
* Handle clicks on the action button.
*
* Fired pending to allow the 'click' event
* to finish its event path before
* dispatching the 'action' event.
*
* @param {Event} e
* @private
*/
onActionButtonClick: function onActionButtonClick() {
var _this6 = this;
debug('action button click');
var config = { detail: { type: this.action } };
var e = new CustomEvent('action', config);
setTimeout(function () {
return _this6.dispatchEvent(e);
});
},
/**
* Get the margin-start value required
* to center the title between
* surrounding buttons.
*
* @param {Object} title
* @return {Object}
* @private
*/
getTitleMarginStart: function getTitleMarginStart() {
var start = this.titleStart;
var end = this.titleEnd;
var marginStart = end - start;
debug('get title margin start', marginStart);
return marginStart;
},
/**
* Get all the buttons (<a> & <button>)
* before the first <h1>.
*
* @return {Array}
* @private
*/
getButtonsBeforeTitle: function getButtonsBeforeTitle() {
var children = this.children;
var l = children.length;
var els = [];
for (var i = 0; i < l; i++) {
var el = children[i];
if (el.tagName === 'H1') {
break;
}
if (!contributesToLayout(el)) {
continue;
}
els.push(el);
}
// Don't forget the action button
if (this.action) {
els.push(this.els.actionButton);
}
return els;
},
/**
* Get all the buttons (<a> & <button>)
* after the last <h1>.
*
* @return {Array}
* @private
*/
getButtonsAfterTitle: function getButtonsAfterTitle() {
var children = this.children;
var els = [];
for (var i = children.length - 1; i >= 0; i--) {
var el = children[i];
if (el.tagName === 'H1') {
break;
}
if (!contributesToLayout(el)) {
continue;
}
els.push(el);
}
return els;
},
/**
* Get the sum of the width of
* the given buttons.
*
* This function is optimized to avoid reading
* `element.clientWidth` when possible.
*
* If a button is `display: none` it will
* have a `.clientWidth` of 0, therefore won't
* contribute anything to the overall sum.
*
* @param {Array} buttons
* @return {Number}
* @private
*/
sumButtonWidths: function sumButtonWidths(buttons) {
var _this7 = this;
var defaultWidth = 50;
var sum = buttons.reduce(function (prev, button) {
var isStandardButton = button === _this7.els.actionButton;
var width = isStandardButton ? defaultWidth : button.clientWidth;
return prev + width;
}, 0);
debug('sum button widths', buttons, sum);
return sum;
},
/**
* Known attribute property
* descriptors.
*
* These setters get called when matching
* attributes change on the element.
*
* @type {Object}
*/
attrs: {
action: {
get: function get() {
return this._action;
},
set: function set(value) {
var action = KNOWN_ACTIONS[value];
if (action === this._action) {
return;
}
this.setAttr('action', action);
this._action = action;
}
},
titleStart: {
get: function get() {
debug('get title-start');
if ('_titleStart' in this) {
return this._titleStart;
}
var buttons = this.getButtonsBeforeTitle();
var value = this.sumButtonWidths(buttons);
debug('get title-start', buttons, value);
return value;
},
set: function set(value) {
debug('set title-start', value);
value = parseInt(value, 10);
if (value === this._titleStart || isNaN(value)) {
return;
}
this.setAttr('title-start', value);
this._titleStart = value;
debug('set');
}
},
titleEnd: {
get: function get() {
debug('get title-end');
if ('_titleEnd' in this) {
return this._titleEnd;
}
var buttons = this.getButtonsAfterTitle();
return this.sumButtonWidths(buttons);
},
set: function set(value) {
debug('set title-end', value);
value = parseInt(value, 10);
if (value === this._titleEnd || isNaN(value)) {
return;
}
this.setAttr('title-end', value);
this._titleEnd = value;
}
},
noFontFit: {
get: function get() {
return this._noFontFit || false;
},
set: function set(value) {
debug('set no-font-fit', value);
value = !!(value || value === '');
if (value === this.noFontFit) {
return;
}
this._noFontFit = value;
if (value) {
this.setAttr('no-font-fit', '');
} else {
this.removeAttr('no-font-fit');
}
}
},
// The [ignore-dir] attribute can be used to force the older behavior of
// gaia-header, where the whole header is always displayed in LTR mode.
ignoreDir: {
get: function get() {
return this._ignoreDir || false;
},
set: function set(value) {
debug('set ignore-dir', value);
value = !!(value || value === '');
if (value === this.ignoreDir) {
return;
}
this._ignoreDir = value;
this.dirObserver = !value;
if (value) {
this.setAttr('ignore-dir', '');
} else {
this.removeAttr('ignore-dir');
}
}
}
},
template: "<div class=\"inner\">\n <button class=\"action-button\">\n <content select=\"[l10n-action]\"></content>\n </button>\n <content></content>\n </div>\n\n <style>\n\n :host {\n display: block;\n -moz-user-select: none;\n\n --gaia-header-button-color:\n var(--header-button-color,\n var(--header-color,\n var(--link-color,\n inherit)));\n }\n\n /**\n * [hidden]\n */\n\n :host[hidden] {\n display: none;\n }\n\n /** Reset\n ---------------------------------------------------------*/\n\n ::-moz-focus-inner { border: 0; }\n\n /** Inner\n ---------------------------------------------------------*/\n\n .inner {\n display: flex;\n min-height: 50px;\n -moz-user-select: none;\n\n background:\n var(--header-background,\n var(--background,\n #fff));\n }\n\n /** Action Button\n ---------------------------------------------------------*/\n\n /**\n * 1. Hidden by default\n */\n\n .action-button {\n position: relative;\n\n display: none; /* 1 */\n width: 50px;\n font-size: 30px;\n margin: 0;\n padding: 0;\n border: 0;\n outline: 0;\n\n align-items: center;\n background: none;\n cursor: pointer;\n transition: opacity 200ms 280ms;\n color:\n var(--header-action-button-color,\n var(--header-icon-color,\n var(--gaia-header-button-color)));\n }\n\n /**\n * [action=back]\n * [action=menu]\n * [action=close]\n *\n * 1. For icon vertical-alignment\n */\n\n [action=back] .action-button,\n [action=menu] .action-button,\n [action=close] .action-button {\n display: flex; /* 1 */\n }\n\n /**\n * :active\n */\n\n .action-button:active {\n transition: none;\n opacity: 0.2;\n }\n\n /** Action Button Icon\n ---------------------------------------------------------*/\n\n .action-button:before {\n font-family: 'gaia-icons';\n font-style: normal;\n text-rendering: optimizeLegibility;\n font-weight: 500;\n }\n\n [action=close] .action-button:before { content: 'close' }\n [action=menu] .action-button:before { content: 'menu' }\n\n [action=back]:-moz-dir(ltr) .action-button:before { content: 'left' }\n [action=back]:-moz-dir(rtl) .action-button:before { content: 'right' }\n\n /** Action Button Icon\n ---------------------------------------------------------*/\n\n /**\n * 1. To enable vertical alignment.\n */\n\n .action-button:before {\n display: block;\n }\n\n /** Action Button Text\n ---------------------------------------------------------*/\n\n /**\n * To provide custom localized content for\n * the action-button, we allow the user\n * to provide an element with the class\n * .l10n-action. This node is then\n * pulled inside the real action-button.\n *\n * Example:\n *\n * <gaia-header action=\"back\">\n * <span l10n-action aria-label=\"Back\">Localized text</span>\n * <h1>title</h1>\n * </gaia-header>\n */\n\n ::content [l10n-action] {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n height: 100%;\n font-size: 0;\n }\n\n /** Title\n ---------------------------------------------------------*/\n\n /**\n * 1. Vertically center text. We can't use flexbox\n * here as it breaks text-overflow ellipsis\n * without an inner div.\n */\n\n ::content h1 {\n flex: 1;\n margin: 0;\n padding: 0;\n overflow: hidden;\n\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: center;\n line-height: 50px; /* 1 */\n font-weight: 300;\n font-style: italic;\n font-size: 24px;\n\n color:\n var(--header-title-color,\n var(--header-color,\n var(--title-color,\n var(--text-color,\n inherit))));\n }\n\n /**\n * [ignore-dir]\n *\n * When the <gaia-header> component has an [ignore-dir] attribute, header\n * direction is forced to LTR but we still want the <h1> text to be reversed\n * so that strings like '1 selected' become 'selected 1'.\n *\n * When we're happy for <gaia-header> to be fully RTL responsive we won't need\n * these rules anymore, but this depends on all Gaia apps being ready.\n *\n * This should be safe to remove when bug 1179459 lands.\n */\n\n :host[ignore-dir] {\n direction: ltr;\n }\n\n :host[ignore-dir]:-moz-dir(rtl) h1 {\n direction: rtl;\n }\n\n /** Buttons\n ---------------------------------------------------------*/\n\n ::content a,\n ::content button {\n position: relative;\n z-index: 1;\n box-sizing: border-box;\n display: flex;\n width: auto;\n height: auto;\n min-width: 50px;\n margin: 0;\n padding: 0 10px;\n outline: 0;\n border: 0;\n\n font-size: 14px;\n line-height: 1;\n align-items: center;\n justify-content: center;\n text-decoration: none;\n text-align: center;\n background: none;\n border-radius: 0;\n font-style: italic;\n cursor: pointer;\n transition: opacity 200ms 280ms;\n color: var(--gaia-header-button-color);\n }\n\n /**\n * :active\n */\n\n ::content a:active,\n ::content button:active {\n transition: none;\n opacity: 0.2;\n }\n\n /**\n * [hidden]\n */\n\n ::content a[hidden],\n ::content button[hidden] {\n display: none;\n }\n\n /**\n * [disabled]\n */\n\n ::content a[disabled],\n ::content button[disabled] {\n pointer-events: none;\n color: var(--header-disabled-button-color);\n }\n\n /** Icon Buttons\n ---------------------------------------------------------*/\n\n /**\n * Icons are a different color to text\n */\n\n ::content .icon,\n ::content [data-icon] {\n color:\n var(--header-icon-color,\n var(--gaia-header-button-color));\n }\n\n /**\n * If users want their action button\n * to be in the component's light-dom\n * they can add an .action class\n * to make it look like the\n * shadow action button.\n */\n\n ::content .action {\n color:\n var(--header-action-button-color,\n var(--header-icon-color,\n var(--gaia-header-button-color)));\n }\n\n /**\n * [data-icon]:empty\n *\n * Icon buttons with no textContent,\n * should always be 50px.\n *\n * This is to prevent buttons being\n * larger than they should be before\n * icon-font has loaded.\n */\n\n ::content [data-icon]:empty {\n width: 50px;\n }\n\n </style>",
// Test hook
nextTick: nextTick
});
/**
* Utils
*/
/**
* Determines whether passed element
* contributes to the layout in gaia-header.
*
* Children with `[l10n-action]` get distributed
* inside the action-button so don't occupy
* space before the `<h1>`.
*
* @param {Element} el
* @return {Boolean}
*/
function contributesToLayout(el) {
return el.localName !== 'style' && !el.hasAttribute('l10n-action');
}
/**
* Set a 'style id' property that
* can be retrieved later.
*
* Used to determine whether a title's
* `style` needs to be updated or not.
*
* @param {Element} el
* @param {String} id
*/
function setStyleId(el, id) {
el._styleId = id;
}
/**
* Get a 'style id' property.
*
* Used to determine whether a title's
* `style` needs to be updated or not.
*
* @param {Element} el
* @param {String} id
*/
function getStyleId(el) {
return el._styleId;
}
/**
* Calls callback at next 'microtask'.
* Returns an object that has
* a `.clear()` method.
*
* @param {Function} fn
* @return {Object} { clear }
*/
function nextTick(fn) {
var cleared;
Promise.resolve().then(function () {
if (!cleared) {
fn();
}
});
return { clear: function clear() {
cleared = true;
} };
}
});
})(typeof define == 'function' && define.amd ? define : (function (n, w) {
'use strict';return typeof module == 'object' ? function (c) {
c(require, exports, module);
} : function (c) {
var m = { exports: {} };c(function (n) {
return w[n];
}, m.exports, m);w[n] = m.exports;
};
})('gaia-header', this));
}, { "font-fit": 1, "gaia-component": 2, "gaia-icons": 3 }] }, {}, [4])(4);
});