All files / coral-component-card/src/scripts Card.js

72.82% Statements 75/103
58.06% Branches 36/62
70.59% Functions 24/34
72.82% Lines 75/103

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428                                            2x                                   2x               2x     2x   2x 10x                     2x 2x   2x           36x   36x   36x             36x   36x       36x                 2x                                   36x   36x   36x   36x 14x     36x 36x   36x 36x   36x 24x   24x 24x       36x 144x   144x 94x         36x 48x   48x         48x         36x 36x 36x 36x   36x     36x 36x                 36x       36x                               36x     36x   36x   36x   36x                                                             36x                                                                         36x         36x   36x       36x                         72x     36x       36x                                                                   36x     36x       36x                                                                     4x         36x 36x   36x   36x   36x 2x     36x         756x                             20x         22x
/**
 * Copyright 2019 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
 
import {BaseComponent} from '../../../coral-base-component';
import base from '../templates/base';
import {commons, transform, validate} from '../../../coral-utils';
import {Decorator} from '../../../coral-decorator';
 
const COLOR_HINT_REG_EXP = /^#[0-9A-F]{6}$/i;
 
/**
 Enumeration for {@link Card} variants.
 
 @typedef {Object} CardVariantEnum
 
 @property {String} DEFAULT
 Default card variant that shows the asset, overlay and content in their default positions.
 @property {String} QUIET
 Quiet card variant that shows the asset, overlay and content in their default positions.
 @property {String} CONDENSED
 Condensed card variant where the overlay is hidden and the content is shown over the image.
 @property {String} INVERTED
 Condensed card variant where the overlay is hidden and the content is shown over the image with a dark style.
 @property {String} ASSET
 Card variant where only the asset is shown.
 */
const variant = {
  DEFAULT: 'default',
  QUIET: 'quiet',
  CONDENSED: 'condensed',
  INVERTED: 'inverted',
  ASSET: 'asset'
};
 
// the card's base classname
const CLASSNAME = '_coral-Card';
 
// builds a string containing all possible variant classnames. this will be used to remove classnames when the variant
// changes
const ALL_VARIANT_CLASSES = [];
for (const variantValue in variant) {
  ALL_VARIANT_CLASSES.push(`${CLASSNAME}--${variant[variantValue]}`);
}
 
/**
 @class Coral.Card
 @classdesc A Card component to display content in different variations.
 @htmltag coral-card
 @extends {HTMLElement}
 @extends {BaseComponent}
 */
const Card = Decorator(class extends BaseComponent(HTMLElement) {
  /** @ignore */
  constructor() {
    super();
 
    // Prepare templates
    this._elements = {
      // Fetch or create the content zone elements
      asset: this.querySelector('coral-card-asset') || document.createElement('coral-card-asset'),
      content: this.querySelector('coral-card-content') || document.createElement('coral-card-content'),
      info: this.querySelector('coral-card-info') || document.createElement('coral-card-info'),
      overlay: this.querySelector('coral-card-overlay') || document.createElement('coral-card-overlay')
    };
    base.call(this._elements);
 
    // Events
    this._delegateEvents({
      'capture:load coral-card-asset img': '_onLoad'
    });
  }
 
  /**
   The Asset of the card.
 
   @type {CardAsset}
   @contentzone
   */
  get asset() {
    return this._getContentZone(this._elements.asset);
  }
 
  set asset(value) {
    this._setContentZone('asset', value, {
      handle: 'asset',
      tagName: 'coral-card-asset',
      insert: function (asset) {
        this.insertBefore(asset, this.info || this._elements.wrapper || null);
      }
    });
  }
 
  /**
   Hints the height of the asset that is going to be loaded. This prepares the size so that when the image is
   loaded no reflow is triggered. Both <code>assetHeight</code> and <code>assetWidth</code> need to be specified
   for this feature to take effect.
 
   @type {String}
   @default ""
   @htmlattribute assetheight
   */
  get assetHeight() {
    return this._assetHeight || '';
  }
 
  set assetHeight(value) {
    this._assetHeight = transform.number(value);
 
    // Avoid a forced reflow by executing following in the next frame
    window.requestAnimationFrame(() => {
      // both hint dimensions need to be set in order to use this feature
      if (!this._loaded && this._elements.asset && this.assetWidth && this._assetHeight) {
        // gets the width without the border of the card
        const clientRect = this.getBoundingClientRect();
        const width = clientRect.right - clientRect.left;
        // calculates the image ratio used to resize the height
        const ratio = width / this.assetWidth;
 
        // the image is considered "low resolution"
        // @todo: check this after removal of lowResolution
        if (ratio > 1) {
          // 32 = $card-asset-lowResolution-padding * 2
          this._elements.asset.style.height = `${this._assetHeight + 32}px`;
        }
        // for non-low resolution images, condensed and inverted cards do not require the height to be set
        else if (this.variant !== variant.CONDENSED && this.variant !== variant.INVERTED) {
          this._elements.asset.style.height = `${ratio * this._assetHeight}px`;
        }
      }E
    });
  }
 
  /**
   Hints the width of the asset that is going to be loaded. This prepares the size so that when the image is
   loaded no reflow is triggered. Both <code>assetHeight</code> and <code>assetWidth</code> need to be specified
   for this feature to take effect.
 
   @type {String}
   @default ""
   @htmlattribute assetwidth
   */
  get assetWidth() {
    return this._assetWidth || '';
  }
 
  set asIsetWidth(value) {
    this._assetWidth = transform.number(value);
  }
 
  /**
   @type {String}
   @default ""
   @htmlattribute colorhint
   */
  get colorHint() {
    return this._colorHint || '';
  }
 
  set colorHint(value) {
    if (COLOR_HINT_REG_EXP.test(value)) {
      this._colorHint = value;
 
      // if the image is already loaded we do not add the color hint to the asset
      if (!this._loaded) {
        this._elements.asset.style['background-color'] = this._colorHint;
      }
    }
  }
 
  /**
   The Content of the card.
 
   @type {CardContent}
   @contentzone
   */
  get content() {
    return this._getContentZone(this._elements.content);
  }
 
  set content(value) {
    this._setContentZone('content', value, {
      handle: 'content',
      tagName: 'coral-card-content',
      insert: function (content) {
        // Ensure title comes first
        const title = content.querySelector('coral-card-title');
        if (title) {
          content.insertBefore(title, content.firstChild);
        }
 
        this._elements.wrapper.insertBefore(content, this.overlay || null);
      }
    });
  }
 
  /**
   The information area of the card, which is placed over all the content. It is typically used for alerts.
 
   @type {CardInfo}
   @contentzone
   */
  get info() {
    retuIrn this._getContentZone(this._elements.info);
  }

  set info(value) {
    this._setContentZone('info', value, {
      handle: 'info',
      tagName: 'coral-card-info',
      insert: function (info) {
        this.appendChild(info);
      }
    });
  }
 
  /**
   Fixes the width of the card. By default cards will take the width of their containers allowing them to interact
   nicely with grids. Whenever they are used standalone fixing the width might be desired.
 
   @type {Boolean}
   @default false
   @htmlattribute fixedwidth
   @htmlattributereflected
   */
  get fixedWidth() {
    return this._fixedWidth || false;
  }
 
  set fixedWidth(value) {
    this._fixedWidth = transform.booleanAttr(value);
    this._reflectAttribute('fixedwidth', this._fixedWidth);
 
    this.classList.toggle(`${CLASSNAME}--fixedWidth`, this._fixedWidth);
  }
 
  /**
   The Overlay of the card.
 
   @type {CardOverlay}
   @contentzone
   */
  get overlay() {
    return this._getContentZone(this._elements.overlay);
  }
 
  set overlay(value) {
    this._setContentZone('overlay', value, {
      handle: 'overlay',
      tagName: 'coral-card-overlay',
      insert: function (overlay) {
        this._elements.wrapper.appendChild(overlay);
      }
    });
  }

  /**
   Whether the card is stacked or not. This is used to represent several assets grouped together.
 
   @type {Boolean}
   @default false
   @htmlattribute stacked
   @htmlattributereflected
   */
  get stacked() {
    return this._stacked || false;
  }

  set stacked(value) {
    this._stacked = transform.booleanAttr(value);
    this._reflectAttribute('stacked', this._stacked);
 
    this.classList.toggle(`${CLASSNAME}--stacked`, this._stacked);
  }
 
  /**
   The card's variant. It determines which sections of the Card and in which position they are shown.
   See {@link CardVariantEnum}.
I
   @type {String}
   @default CardVariantEnum.DEFAULT
   @htmlattribute variant
   */
  get variant() {
    return this._variant || variant.DEFAULT;
  }
 
  set variant(value) {
    value = transform.string(value).toLowerCase();
    this._variant = validate.enumeration(variant)(value) && value || variant.DEFAULT;
    this._reflectAttribute('variant', this._variant);
 
    this.classList.remove(...ALL_VARIANT_CLASSES);
 
    if (this._variant !== variant.DEFAULT) {
      this.classList.add(`${CLASSNAME}--${this._variant}`);
    }
 
    this.assetHeight = this.assetHeight;
  }
 
  /** @ignore */
  _onLoad(event) {
    // @todo fix me for multiple images
    // sets the image as loaded
    this._loaded = true;
 
    // removes the height style since the asset has been completely loaded
    this._elements.asset.style.height = '';
 
    // enables the transition
    event.target.classList.remove('is-loading');
  }
 
  get _contentZones() {
    return {
      'coral-card-asset': 'asset',
      'coral-card-content': 'content',
      'coral-card-info': 'info',
      'coral-card-overlay': 'overlay'
    };
  }
 
  /**
   Returns {@link Card} variants.

   @return {CardVariantEnum}
   */
  static get variant() {
    return variant;
  }
 
  static get _attributePropertyMap() {
    return commons.extend(super._attributePropertyMap, {
      assetwidth: 'assetWidth',
      assetheight: 'assetHeight',
      colorhint: 'colorHint',
      fixedwidth: 'fixedWidth'
    });
  }
 
  /** @ignore */
  static get observedAttributes() {
    return super.observedAttributes.concat([
      'assetwidth',
      'assetheight',
      'colorhint',
      'fixedwidth',
      'variant',
      'stacked'
    ]);
  }
 
  /** @ignore */
  render() {
    super.render();
 
    this.classList.add(CLASSNAME);

    // Default reflected attributes
    if (!this._variant) {
      this.variant = variant.DEFAULT;
    }

    const content = this._elements.content;
    const asset = this._elements.asset;
 
    // Prepares images to be loaded nicely
    const images = asset.querySelectorAll('img');
    const imagesCount = images.length;
    for (let i = 0 ; i < imagesCount ; i++) {
      const image = images[i];
      if (!image.complete) {
        image.classList.add('is-loading');
      }
    }
 
    for (const contentZone in this._contentZones) {
      const element = this._elements[this._contentZones[contentZone]];
      // Remove it so we can process children
      if (element.parentNode) {
        element.parentNode.removeChild(element);
      }
    }

    // Moves everything into the main content zone
    while (this.firstChild) {
      const child = this.firstChild;
      // Removes the empty spaces
      if (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== '' ||
        child.nodeType === Node.ELEMENT_NODE && child.getAttribute('handle') !== 'wrapper') {
        // Add non-template elements to the content
        content.appendChild(child);
      }
      // Remove anything else element
      else {
        this.removeChild(child);
      }
    }
 
    // Assign the content zones so the insert functions will be called
    this.overlay = this._elements.overlay;
    this.content = content;
    this.info = this._elements.info;
 
    this.appendChild(this._elements.wrapper);
 
    // The 'asset' setter knows to insert the element just before the wrapper node.
    this.asset = asset;
 
    // In case a lot of alerts are added, they will not overflow the card
    // Also check whether any alerts are available
    requestAnimationFrame(()=> {
      this.classList.toggle(`${CLASSNAME}--overflow`, this.info.childNodes.length && this.info.scrollHeight > this.clientHeight);
    });
  }
});
 
export default Card;