diff --git a/dist/gaia-header.js b/dist/gaia-header.js index 03e2440..acd981b 100644 --- a/dist/gaia-header.js +++ b/dist/gaia-header.js @@ -167,6 +167,8 @@ require('gaia-icons'); */ var actionTypes = { menu: 1, back: 1, close: 1 }; +const KNOWN_ATTRIBUTES = ['action', 'skip-init', 'start', 'end']; + /** * Register the component. * @@ -183,6 +185,11 @@ module.exports = Component.register('gaia-header', { * @private */ created: function() { + this.attrs = {}; + this.runFontFitTimeout = null; + + KNOWN_ATTRIBUTES.forEach((name) => this._updateAttribute(name)); + this.createShadowRoot().innerHTML = this.template; // Get els @@ -194,7 +201,22 @@ module.exports = Component.register('gaia-header', { this.els.actionButton.addEventListener('click', e => this.onActionButtonClick(e)); this.configureActionButton(); + }, + + /** + * Initializes the component. + * It especially runs the font-fit algorithm once, and registers the + * textContent observer. + * + * @private + */ + init: function() { + if (this.attrs.skipInit !== null) { + return; + } + this.runFontFit(); + this.addFontFitObserver(); }, /** @@ -204,7 +226,14 @@ module.exports = Component.register('gaia-header', { * @private */ attached: function() { - this.rerunFontFit(); + this.init(); + }, + + /** + * Called when the element is detached from the DOM + */ + detached: function() { + this.removeFontFitObserver(); }, /** @@ -214,35 +243,91 @@ module.exports = Component.register('gaia-header', { * @private */ attributeChanged: function(attr) { + if (KNOWN_ATTRIBUTES.indexOf(attr) === -1) { + return; + } + + this._updateAttribute(attr); + + if (attr === 'skip-init') { + setTimeout(() => this.init()); + return; + } + if (attr === 'action') { this.configureActionButton(); - this.rerunFontFit(); } + + this.runFontFitSoon(); }, /** - * Runs the logic to size and position - * header text inside the available space. + * Used to camel case a word containing dashes * * @private */ - runFontFit: function() { + _camelCase: function ut_camelCase(str) { + return str.replace(/-(.)/g, function replacer(str, p1) { + return p1.toUpperCase(); + }); + }, + + /** + * Updates an attribute value in the internal attrs object + * + * @private + */ + _updateAttribute: function(name) { + var newVal = this.getAttribute(name); + this.attrs[this._camelCase(name)] = newVal; + }, + + /** + * Adds the textContent observer in the font fit library. + * + * @private + */ + addFontFitObserver: function() { for (var i = 0; i < this.els.headings.length; i++) { - fontFit.reformatHeading(this.els.headings[i]); fontFit.observeHeadingChanges(this.els.headings[i]); } }, /** - * Rerun font-fit logic. + * Removes the textContent observer in the font fit library. * - * TODO: We really need an official API for this. + * @private + */ + removeFontFitObserver: function() { + fontFit.disconnectHeadingObserver(); + }, + + /** + * This function will debounce the use of runFontFit. This is used in + * attributeChanged so that the component user can change different attributes + * and still have runFontFit run once. * * @private */ - rerunFontFit: function() { + runFontFitSoon: function() { + clearTimeout (this.runFontFitTimeout); + this.runFontFitTimeout = setTimeout(() => this.runFontFit()); + }, + + /** + * Runs the logic to size and position + * header text inside the available space. + * + * @private + */ + runFontFit: function() { for (var i = 0; i < this.els.headings.length; i++) { - fontFit.reformatHeading(this.els.headings[i]); + var heading = this.els.headings[i]; + var start = parseInt(this.attrs.start); + var end = parseInt(this.attrs.end); + start = isNaN(start) ? null : start; + end = isNaN(end) ? null : end; + fontFit.reformatHeading(heading, start, end); } }, @@ -253,7 +338,7 @@ module.exports = Component.register('gaia-header', { * @public */ triggerAction: function() { - if (this.isSupportedAction(this.getAttribute('action'))) { + if (this.isSupportedAction(this.attrs.action)) { this.els.actionButton.click(); } }, @@ -267,7 +352,7 @@ module.exports = Component.register('gaia-header', { */ configureActionButton: function() { var old = this.els.actionButton.getAttribute('icon'); - var type = this.getAttribute('action'); + var type = this.attrs.action; var supported = this.isSupportedAction(type); this.els.actionButton.classList.remove('icon-' + old); this.els.actionButton.setAttribute('icon', type); @@ -281,7 +366,7 @@ module.exports = Component.register('gaia-header', { * @private */ isSupportedAction: function(action) { - return action && actionTypes[action]; + return !!(action && actionTypes[action]); }, /** @@ -295,7 +380,7 @@ module.exports = Component.register('gaia-header', { * @private */ onActionButtonClick: function(e) { - var config = { detail: { type: this.getAttribute('action') } }; + var config = { detail: { type: this.attrs.action } }; var actionEvent = new CustomEvent('action', config); setTimeout(this.dispatchEvent.bind(this, actionEvent)); }, @@ -583,6 +668,22 @@ return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this)); },{"./lib/font-fit":4,"gaia-component":1,"gaia-icons":2}],4:[function(require,module,exports){ ;(function(define){'use strict';define(function(require,exports,module){ + var privMap = new WeakMap(); + + function getPriv(instance) { + var privMembers = privMap.get(instance); + + if (!privMembers) { + privMembers = { + start: null, + end: null + }; + privMap.set(instance, privMembers); + } + + return privMembers; + } + /** * Utility functions for measuring and manipulating font sizes */ @@ -605,23 +706,61 @@ return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this)); observer.observe(heading, { childList: true }); }, + /** + * Stops the observer from observing the heading changes. + */ + disconnectHeadingObserver: function() { + var observer = this._getTextChangeObserver(); + observer.disconnect(); + }, + /** * Resize and reposition the header text based on string length and - * container position. + * container position * * @param {HTMLHeadingElement} heading h1 text inside header to reformat. + * @param {Number} start Offset in pixels between the start of the text + * container and the start of the inner window. + * @param {Number} end Offset in pixels between the end of the text + * container and the end of the inner window. */ - reformatHeading: function(heading) { + reformatHeading: function(heading, start, end) { // Skip resize logic if header has no content, ie before localization. if (!heading || heading.textContent.trim() === '') { return; } + var style; + var priv = getPriv(heading); + if (start !== undefined) { + priv.start = start; + } + + if (end !== undefined) { + priv.end = end; + } + + start = priv.start; + end = priv.end; + + var hasSizeInformation = start !== null || end !== null; + // Reset our centering styles. this._resetCentering(heading); - // Cache the element style properties to avoid reflows. - var style = this._getStyleProperties(heading); + if (hasSizeInformation) { + style = { + fontFamily: 'sans-serif', + contentWidth: this._getWindowWidth() - (start || 0) - (end || 0), + paddingRight: 0, + paddingLeft: 0, + offsetLeft: start || 0, + rtlFriendly: true + }; + } else { + // Cache the element style properties to avoid reflows. + style = this._getStyleProperties(heading); + } // If the document is inside a hidden iframe // `window.getComputedStyle()` returns null, @@ -858,6 +997,7 @@ return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this)); // We need to set the lateral margins to 0 to be able to measure the // element width properly. All previously set values are ignored. heading.style.marginLeft = heading.style.marginRight = '0'; + heading.style.MozMarginStart = heading.style.MozMarginEnd = '0'; }, /** @@ -891,6 +1031,20 @@ return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this)); return; } + var propLeft, propRight; + if (styleOptions.rtlFriendly) { + propLeft = 'MozMarginStart'; + propRight = 'MozMarginEnd'; + } else { + propLeft = 'marginLeft'; + propRight = 'marginRight'; + } + + // reset the previous value + ['marginLeft', 'marginRight', 'MozMarginStart', 'MozMarginEnd'].forEach( + (prop) => delete heading.style[prop] + ); + // To center, we need to make sure the space to the left of the header // is the same as the space to the right, so take the largest of the two. var margin = Math.max(sideSpaceLeft, sideSpaceRight); @@ -901,10 +1055,10 @@ return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this)); // See https://bugzil.la/1026955 if (minHeaderWidth + (margin * 2) < this._getWindowWidth() - 1) { if (sideSpaceLeft < sideSpaceRight) { - heading.style.marginLeft = (sideSpaceRight - sideSpaceLeft) + 'px'; + heading.style[propLeft] = (sideSpaceRight - sideSpaceLeft) + 'px'; } if (sideSpaceRight < sideSpaceLeft) { - heading.style.marginRight = (sideSpaceLeft - sideSpaceRight) + 'px'; + heading.style[propRight] = (sideSpaceLeft - sideSpaceRight) + 'px'; } } },