codevise/pageflow

View on GitHub
entry_types/scrolled/package/src/editor/models/ScrolledEntry/moveContentElement.js

Summary

Maintainability
A
2 hrs
Test Coverage
import {Batch} from './Batch';
import {maybeMergeWithAdjacent} from './maybeMergeWithAdjacent';

// Move content element inside section or between sections. Allow
// moving content elements to "split points" inside content elements
// with custom split functions (e.g. between two paragraphs of a text
// block). Merge content elements of the same type that become
// adjacent by moving a content element away (e.g. two text blocks
// surrounding an image that is moved away).
export function moveContentElement(entry, contentElement, {range, sibling, at, splitPoint}) {
  const sourceBatch = new Batch(entry, contentElement.section);

  // If we move content elements between sections, merges will need to
  // be performed in the section where the content element came from.
  const targetBatch = sibling.section === contentElement.section ?
                      sourceBatch :
                      new Batch(entry, sibling.section);

  if (range && !rangeCoversWholeElement(sourceBatch, contentElement, range)) {
    if (contentElement === sibling && at === 'split') {
      // If we are moving part of a content element inside the content
      // element itself, we need to adjust the split point if the
      // moved range lies above the split point since moving a range
      // means first extracting/removing it from the source element.
      const delta = (splitPoint > range[0]) ? range[1] - range[0] : 0;
      splitPoint -= delta;
    }

    contentElement = extractRange(sourceBatch, contentElement, range)
  }

  if (at === 'split') {
    // When moving a content element to a split point in the adjacent
    // element below, insert split off element before sibling so that
    // is can directly be merged again. For example, let X be a
    // content element in between two text blocks T1 and T2:
    //
    //   T1
    //     paragraph A
    //   X
    //   T2
    //     paragraph B
    //     paragraph C
    //
    // When X shall be moved between the two paragraphs of T2, we want
    // to split off the first paragraph of T2 into a new content
    // element T3 and move X:
    //
    //   T1
    //     T1 paragraph A
    //   X
    //   T3
    //     T1 paragraph B
    //   T2
    //     T2 paragraph C
    //
    // T3 becomes the new sibling that we want to move X after:
    //
    //   T1
    //     T1 paragraph A
    //   T3
    //     T1 paragraph B
    //   X
    //   T2
    //     T2 paragraph C
    //
    // When we later merge T1 and T3, T1 will be updated making T3
    // disappear again without ever persisting it to the server:
    //
    //   T1
    //     T1 paragraph A
    //     T1 paragraph B
    //   X
    //   T2
    //     T2 paragraph C
    //
    if (sourceBatch.getAdjacent(contentElement)[1] === sibling) {
      sibling = targetBatch.split(sibling, splitPoint, {insertAt: 'before'});
    }
    else {
      targetBatch.split(sibling, splitPoint);
    }
  }

  const [before, after] = sourceBatch.getAdjacent(contentElement);
  let targetRange;

  // Check if element was dragged to same position where it came from.
  if (!(at === 'before' && !range && sibling === after) &&
      !(at === 'after' && !range && sibling === before)) {
    sourceBatch.remove(contentElement);

    if (at === 'before') {
      targetBatch.insertBefore(sibling, contentElement);
    }
    else {
      targetBatch.insertAfter(sibling, contentElement);
    }

    [contentElement, targetRange] = maybeMergeWithAdjacent(
      targetBatch,
      contentElement
    );

    sourceBatch.maybeMerge(before, after);
  }

  // Dragging an element next to a sticky element, shall make the
  // moved element sticky as well.
  copyPositionIfAvailable(targetBatch, contentElement, sibling);

  targetBatch.saveIfDirty({
    success() {
      entry.trigger('selectContentElement', contentElement, {
        range: targetRange
      });
    }
  });
  sourceBatch.saveIfDirty();
}

function rangeCoversWholeElement(batch, contentElement, range) {
  return range[0] === 0 && range[1] === batch.getLength(contentElement);
}

function extractRange(batch, contentElement, range) {
  const extracted = batch.split(contentElement, range[0]);
  const suffix = batch.split(extracted, range[1] - range[0]);

  batch.maybeMerge(contentElement, suffix);

  return extracted;
}

function copyPositionIfAvailable(batch, contentElement, sibling) {
  if (contentElement.getPosition() !== sibling.getPosition() &&
      contentElement.getAvailablePositions().includes(sibling.getPosition())) {
    batch.markForUpdate(contentElement, {
      ...contentElement.configuration.toJSON,
      position: sibling.getPosition()
    });
  }
}