All files / coral-component-masonry/src/scripts MasonryColumnLayout.js

82.42% Statements 150/182
69.12% Branches 47/68
76.19% Functions 16/21
82.42% Lines 150/182

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 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463                                                        2x 2x   2x                 70x   70x 70x   70x   70x   70x   70x   70x   70x   70x     70x 70x 70x 70x 70x 70x 70x                   2x                       145x 145x 145x 145x   145x 145x   145x 32x 32x 32x 32x 32x 32x   113x 113x 113x 113x 113x 113x       145x 145x   145x 530x             145x 642x 642x   642x 216x       642x                       145x 642x 642x   642x   642x 216x 216x     642x                                                                     145x 642x 642x 642x 642x                           145x 642x 642x   642x         642x 642x 642x   642x   2078x   2078x 2102x     2078x 1023x 1023x       642x 642x   642x 216x 216x 216x 216x 216x       642x   642x 666x 666x 666x                                             157x 530x                                           4x   4x         4x 4x   4x             4x   4x 2x   2x   2x 2x         2x 2x   2x 2x       4x 4x 4x                       4x   4x           4x   4x   4x 2x 2x   2x 2x     4x 4x 4x                                                                                         157x 157x   157x     145x   145x   145x   145x   12x       157x   157x   157x 6x     151x               2x   2x   2x 18x 18x 18x   18x  
/**
 * 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 MasonryLayout from './MasonryLayout';
import {setTransition, setTransform, csspx, getPositiveNumberProperty} from './MasonryLayoutUtil';
import {Keys} from '../../../coral-utils';
 
/**
 Base class for column-based masonry layouts.
 
 @class Coral.Masonry.ColumnLayout
 @classdesc A Masonry Column layout
 @extends {MasonryLayout}
 */
class MasonryColumnLayout extends MasonryLayout {
  /**
   Takes a {Masonry} instance as argument.
 
   @param {Masonry} masonry
   */
  constructor(masonry) {
    super(masonry);
 
    this._columns = [];
 
    const up = this._moveFocusVertically.bind(this, true);
    const down = this._moveFocusVertically.bind(this, false);
    const left = this._moveFocusHorizontally.bind(this, true);
    const right = this._moveFocusHorizontally.bind(this, false);
    const home = this._moveFocusHomeEnd.bind(this, true);
    const end = this._moveFocusHomeEnd.bind(this, false);
 
    const keys = this._keys = new Keys(masonry, {
      context: this
    });
    keys.on('up', up).on('k', up);
    keys.on('down', down).on('j', down);
    keys.on('left', left).on('h', left);
    keys.on('right', right).on('l', right);
    keys.on('home', home);
    keys.on('end', end);
  }
 
  /**
   Hook to remove layout specific style and data from the item.
 
   @param item
   @private
   */
  // eslint-disable-next-line no-unused-vars
  _resetItem(item) {
    // To override
  }
 
  /**
   Initialize layout variables.
 
   @private
   */
  _init(items) {
    const firstItem = items[0];
    const masonry = this._masonry;
    this._columnWidth = getPositiveNumberProperty(masonry, 'columnWidth', 'columnwidth', 200);
 
    this._zeroOffsetLeft = -csspx(firstItem, 'marginLeft');
    // with padding
    this._masonryInnerWidth = masonry.clientWidth;
 
    const spacing = this._masonry.spacing;
    if (typeof spacing === 'number') {
      this._horSpacing = spacing;
      this._verSpacing = spacing;
      this._offsetLeft = spacing + this._zeroOffsetLeft;
      this._offsetTop = spacing - csspx(firstItem, 'marginTop');
      this._verPadding = 2 * spacing;
      this._masonryAvailableWidth = masonry.clientWidth - spacing;
    } else {
      this._horSpacing = csspx(firstItem, 'marginLeft') + csspx(firstItem, 'marginRight');
      this._verSpacing = csspx(firstItem, 'marginTop') + csspx(firstItem, 'marginBottom');
      this._offsetLeft = csspx(masonry, 'paddingLeft');
      this._offsetTop = csspx(masonry, 'paddingTop');
      this._verPadding = this._offsetTop + this._verSpacing + csspx(masonry, 'paddingBottom');
      this._masonryAvailableWidth = masonry.clientWidth - this._offsetLeft - csspx(masonry, 'paddingRight');
    }
 
    // Initialize column objects
    const columnCount = Math.max(1, Math.floor(this._masonryAvailableWidth / (this._columnWidth + this._horSpacing)));
    this._columns.length = columnCount;
    for (let ci = 0 ; ci < columnCount ; ci++) {
      this._columns[ci] = {
        height: this._offsetTop,
        items: []
      };
    }
 
    // Prepare layout data
    for (let ii = 0 ; ii < items.length ; ii++) {
      const item = items[ii];
 
      let layoutData = item._layoutData;
      if (!layoutData) {
        item._layoutData = layoutData = {};
      }
 
      // Read colspan
      layoutData.colspan = Math.min(getPositiveNumberProperty(item, 'colspan', 'colspan', 1), this._columns.length);
    }
  }
 
  /**
   Updates the width of all items.
 
   @param items
   @private
   */
  _writeStyles(items) {
    for (let i = 0 ; i < items.length ; i++) {
      const item = items[i];
      const layoutData = item._layoutData;
 
      // Update width
      const itemWidth = Math.round(this._getItemWidth(layoutData.colspan));
      if (layoutData.width !== itemWidth) {
        item.style.width = `${itemWidth}px`;
        layoutData.width = itemWidth;
      }
      this._writeItemStyle(item);
    }
  }
 
  /**
   @param colspan column span of the item
   @return the width of the item for the given colspan
   @private
   */
  // eslint-disable-next-line no-unused-vars
  _getItemWidth(colspan) {
    // To override
  }
 
  /**
   Hook to execute layout specific item preparation.
 
   @param item
   @private
   */
  // eslint-disable-next-line no-unused-vars
  _writeItemStyle(item) {
    // To override
  }
 
  /**
   Reads the dimension of all items.
 
   @param items
   @private
   */
  _readStyles(items) {
    // Record size of items in a separate loop to avoid unneccessary reflows
    for (let i = 0 ; i < items.length ; i++) {
      const item = items[i];
      const layoutData = item._layoutData;
      layoutData.height = Math.round(item.getBoundingClientRect().height);
      layoutData.ignored = layoutData.detached || !item.offsetParent;
    }
  }
 
  /**
   Update the position of all items.
 
   @param items
   @private
   */
  _positionItems(items) {
    let j;
 
    for (let i = 0 ; i < items.length ; i++) {
      const item = items[i];
      const layoutData = item._layoutData;
      // Skip ignored items
      if (layoutData.ignored) {
        continue;
      }
 
      // Search for column with the least height
      const maxLength = this._columns.length - (layoutData.colspan - 1);
      let minColumnIndex = -1;
      let minColumnHeight;
      for (j = 0 ; j < maxLength ; j++) {
        // can be negative if set spacing < item css margin
        let columnHeight = this._offsetTop;
        for (let y = 0 ; y < layoutData.colspan ; y++) {
          columnHeight = Math.max(columnHeight, this._columns[j + y].height);
        }
        if (minColumnIndex === -1 || columnHeight < minColumnHeight) {
          minColumnIndex = j;
          minColumnHeight = columnHeight;
        }
      }
 
      const top = minColumnHeight;
      const left = Math.round(this._getItemLeft(minColumnIndex));
 
      // Check if position has changed
      ifI (layoutData.left !== left || layoutData.top !== top) {
        layoutData.columnIndex = minColumnIndex;
        layoutData.itemIndex = this._columns[minColumnIndex].items.length;
        layoutData.left = left;
        layoutData.top = top;
 
        setTransform(item, `translate(${left}px, ${top}px)`);
      }
 
      // Remember new column height to position all other items
      const newColumnHeight = top + layoutData.height + this._verSpacing;
      for (j = 0 ; j < layoutData.colspan ; j++) {
        const column = this._columns[minColumnIndex + j];
        column.height = newColumnHeight;
        column.items.push(item);
      }
    }
  }
 
  /**
   @param columnIndex
   @return the left position for the given column index
   @private
   */
  // eslint-disable-next-line no-unused-vars
  _getItemLeft(columnIndex) {
    // To override
  }
 
  /**
   @returns {number} the height of the content (independent of the current gird container height)
   @private
   */
  _getContentHeight() {
    return this._columns.reduce((height, column) => Math.max(height, column.height), 0) - this._offsetTop;
  }
 
  /**
   Hook which is called after the positioning is done.
 
   @param contentHeight
   @private
   */
  // eslint-disable-next-line no-unused-vars
  _postLayout(contentHeight) {
    // To override
  }
 
  /**
   Moves the focus vertically.
 
   @private
   */
  _moveFocusVertically(up, event) {
    const currentLayoutData = event.target._layoutData;
    if (!currentLayoutData) {
      return;
    }
 
    // Choose item above or below
    const nextItemIndex = currentLayoutData.itemIndex + (up ? -1 : 1);
    let nextItem = this._columns[currentLayoutData.columnIndex].items[nextItemIndex];
 
    if (nextItem) {
      nextItem.focus();
      // prevent scrolling at the same time
      event.preventDefault();
    } else {
      // in case there is no item in the same column, we should move to first item in next column for down
      // and last item of previous column for up key
      let columnIndex = currentLayoutData.columnIndex;
      if (up) {
        if (columnIndex > 0) {
          // move to last item of previous column
          let prevColumn = this._columns[columnIndex - 1];
          if (prevColumn) {
            nextItem = prevColumn.items[prevColumn.items.length - 1]; // last item of previous column
          }
        }
      } else {
        // down key is pressed, go to first item of next column if exists
        let columnCount = this._columns.length;
        let nextColumnIndex = columnIndex + currentLayoutData.colspan;
        if (nextColumnIndex < columnCount) {
          nextItem = this._columns[nextColumnIndex].items[0]; // first item of next column
        }
      }
      if (nextItem) {
      I  nextItem.focus();
        event.preventDefault(); // prevent scrolling at the same time
      }
    }
  }
 
  /**
   Moves the focus horizontally.
I
   @private
   */
  _moveFocusHorizontally(left, event) {
    const currentLayoutData = event.target._layoutData;
    if (!currentLayoutData) {
      return;
    }
 
    let nextItem;
    let itEems = this._masonry.items.getAll();
    let collectionItemIndex = items.indexOf(event.target);
 
    if (left) {
      if (coEllectionItemIndex > 0) {
        nextItem = items[collectionItemIndex - 1];
      }
    } else if (collectionItemIndex < items.length - 1) {
      nextItem = items[collectionItemIndex + 1];
    }
 
    if (nextItem) {
      nextItem.focus();
      evenEt.preventDefault(); // prevent scrolling at the same time
    }
  }
 
  /**
   MovesE the focus to first or last item based on the visual order.
 
   @private
   */
  _moveFocusHomeEnd(home, event) {
    const currentLayoutData = event.target._layoutData;
    if (!currentLayoutData) {
      return;
    }
 
    let nextItem;
    const columns = this._columns;
 
    // when home is pressed, we take the first item of the first column
    if (home) {
      nextItem = columns[0] && columns[0].items[0];
    } Ielse {
      // when end is pressed, we take the last item of the last column; since some columns are empty, we need to
      // iterate backwards to find the first column that has items
      for (let i = columns.length - 1 ; i > -1 ; i--) {
        // since we found a column with items, we take the last item as the next one
        if (columns[i].items.length > 0) {
          nextItem = columns[i].items[columns[i].items.length - 1];
          break;
        }
      }
    }
E
    if (nextItem) {
      nextItem.focus();
      // we pErevent the scrolling
      event.preventDefault();
    }
  }
E
  /** @inheritdoc */
  layout(secondTry) {
    const masonry = this._masonry;
 
    const items = masonry.items.getAll();
    if (items.length > 0) {
      // For best possible performance none of these function calls must both read and write attributes in a loop to
      // avoid unnecessary reflows.
      this._init(items);
      this._writeStyles(items);
      this._readStyles(items);
      this._positionItems(items);
    } else {
      this._columns.length = 0;
    }

    // Update the height of the masonry (otherwise it has a height of 0px due to the absolutely positioned items)
    const contentHeight = this._getContentHeight();
    masonry.style.height = `${contentHeight - this._verSpacing + this._verPadding}px`;

    // Check if the masonry has changed its width due to the changed height (can happen because of appearing/disappearing scrollbars)
    if (!secondTry && this._masonryInnerWidth !== masonry.clientWidth) {
      this.layout(true);
    } else {
      // Post layout hook for sub classes
      this._postLayout(contentHeight);
    }
  }

  /** @inheritdoc */
  destroy() {
    this._keys.destroy();
 
    const items = this._masonry.items.getAll();
    for (let i = 0 ; i < items.length ; i++) {
      const item = items[i];
      item._layoutData = undefined;
      setTransform(item, '');
      this._resetItem(item);
    }
  }
 
  /** @inheritdoc */
  detach(item) {
    item._layoutData.detached = true;
  }
 
  /** @inheritdoc */
  reattach(item) {
    const layoutData = item._layoutData;
    layoutData.detached = false;
 
    const rect = item.getBoundingClientRect();
    // Disable transition while repositioning
    setTransition(item, 'none');
    item.style.left = '';
    item.style.top = '';
    setTransform(item, '');
 
    const nullRect = item.getBoundingClientRect();
    layoutData.left = rect.left - nullRect.left;
    layoutData.top = rect.top - nullRect.top;
    setTransform(item, `translate(${layoutData.left}px, ${layoutData.top}px)`);
    // Enforce position
    item.getBoundingClientRect();
    // Enable transition again
    setTransition(item, '');
  }
 
  /** @inheritdoc */
  itemAt(x, y) {
    // TODO it would be more efficient to pick first the right column
    const items = this._masonry.items.getAll();
 
    for (let i = 0 ; i < items.length ; i++) {
      const item = items[i];
      const layoutData = item._layoutData;
 
      if (layoutData && !layoutData.ignored && (
        layoutData.left <= x && layoutData.left + layoutData.width >= x &&
        layoutData.top <= y && layoutData.top + layoutData.height >= y)) {
        return item;
      }
    }
 
    return null;
  }
}
 
export default MasonryColumnLayout;