util.js

/**
 * Utility functions
 * @module Util
 */

import _ from "lodash";
import { transform, isEqual, isObject } from "lodash";

// For some reason, the `draggable` import has to be in a different file
// from `main.js`.  This has something to do with the way ES6 imports work,
// and the fact that `svg.draggable.js` expects the `SVG` variable to be
// globally available.
import * as SVG from "svg.js";
import * as draggable from "svg.draggable.js";

/**
 * Get all the CSS rules that match the given elements
 * Adapted from:
 * https://stackoverflow.com/questions/2952667/find-all-css-rules-that-apply-to-an-element
 *
 * @param {Array} elements - Array of elements to get rules for
 * @return {Array}
 * @memberof module:Util
 */
function getCssRules(elements) {
  const sheets = document.styleSheets;
  const ret = [];
  let importRules = [];

  for (const sheet of sheets) {
    try {
      const rules = sheet.rules || sheets.cssRules;
      for (const rule of rules) {
        // Include @import rules by default, since we can't be sure if they
        // apply, and since they are generally used for fonts
        if (rule.type === CSSRule.IMPORT_RULE) {
          importRules.push(rule.cssText);
          continue;
        }

        // For other types of rules, check against the listed elements
        for (const el of elements) {
          el.matches =
            el.matches ||
            el.webkitMatchesSelector ||
            el.mozMatchesSelector ||
            el.msMatchesSelector ||
            el.oMatchesSelector;
          if (el.matches(rule.selectorText)) {
            ret.push(rule.cssText);
            break;
          }
        }
      }
    } catch (err) {
      // Sometimes we get CORS errors with Chrome and external stylesheets,
      // but we should be all right to keep going
      console.log("Warning:", err);
    }
  }

  // Import rules have to be at the top of the styles list
  return _.uniq(importRules.concat(ret));
}

/**
 * Sort some given array of Links in preparation for determining their slots
 * (vertical intervals for overlapping/crossing Links).  Needed because the
 * order that the Parser puts Links in might not be the order we actually want:
 *
 * 1) Primary sort by index of left endpoint, ascending
 * 2) Secondary sort by number of Words covered, descending
 *
 * @param links
 * @memberof module:Util
 */
function sortForSlotting(links) {
  const sortingArray = links.map((link, idx) => {
    const endpoints = link.endpoints;
    return {
      idx,
      leftAnchor: endpoints[0].idx,
      width: endpoints[1].idx - endpoints[0].idx + 1
    };
  });
  // Sort by number of words covered, descending
  sortingArray.sort((a, b) => b.width - a.width);
  // Sort by index of left endpoint, ascending
  sortingArray.sort((a, b) => a.leftAnchor - b.leftAnchor);
  return sortingArray.map((link) => links[link.idx]);
}

/**
 * Deep diff between two object, using lodash
 * @param  {Object} object Object compared
 * @param  {Object} base   Object to compare with
 * @return {Object}        Return a new object who represent the diff
 */
function difference(object, base) {
  return transform(object, (result, value, key) => {
    if (!isEqual(value, base[key])) {
      result[key] =
        isObject(value) && isObject(base[key])
          ? difference(value, base[key])
          : value;
    }
  });
}

export default {
  getCssRules,
  sortForSlotting,
  difference
};