You are here

views_isotope.module in Brainstorm profile 7

Load the isotope library and provide configuration and theme options.

File

modules/custom/views_isotope/views_isotope.module
View source
<?php

/**
 * @file
 * Load the isotope library and provide configuration and theme options.
 */
define('VIEWS_ISOTOPE_CDN_PATH', 'https://cdnjs.cloudflare.com/ajax/libs/jquery.isotope/2.2.2/isotope.pkgd.min.js');
define('VIEWS_ISOTOPE_FILENAME', 'isotope.pkgd.min.js');

/**
 * Implements hook_permission().
 */
function views_isotope_permission() {
  return [
    'administer isotope' => [
      'title' => t('Administer Isotope Configuration'),
      'description' => t('Set configuration options for Isotope'),
    ],
  ];
}

/**
 * Implements hook_ctools_plugin_api().
 *
 * Tell CTools that we support the default_views_isotope_preset API.
 */
function views_isotope_ctools_plugin_api($owner, $api) {
  if ($owner == 'views_isotope' && $api == 'default_isotope_configurations') {
    return [
      'version' => 1,
    ];
  }
}

/**
 * Implements hook_default_views_isotope_preset().
 *
 * Provide a couple of default presets.
 */
function views_isotope_default_isotope_configuration() {
  $export = [];
  $config = new stdClass();
  $config->api_version = 1;
  $config->name = 'isotope_default_config';
  $config->admin_title = 'Default config';
  $config->layoutMode = 'masonry';
  $config->transitionDuration = NULL;
  $config->urlFilters = NULL;
  $config->isFitWidth = NULL;
  $config->isHorizontal = NULL;
  $config->stamp = '.stamp';
  $config->horizontalAlignment = NULL;
  $config->verticalAlignment = NULL;
  $config->isOriginLeft = 1;

  // If imagesLoaded is available, add it by default.
  if (views_isotope_check_additional_libraries('imagesLoaded')) {
    $config->plugins = [
      'imagesLoaded',
    ];
  }
  else {
    $config->plugins = [];
  }
  $export['isotope_default_config'] = $config;
  return $export;
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function views_isotope_ctools_plugin_directory($module, $type) {

  // Load the export_ui plugin.
  if ($type == 'export_ui') {
    return 'plugins/export_ui';
  }
}

/**
 * Implements hook_context_plugins().
 */
function views_isotope_context_plugins() {
  $plugins = [];
  $plugins['IsotopeReaction'] = [
    'handler' => [
      'path' => drupal_get_path('module', 'views_isotope') . '/plugins/context',
      'file' => 'views_isotope_reaction.inc',
      'class' => 'IsotopeReaction',
      'parent' => 'context_reaction',
    ],
  ];
  return $plugins;
}

/**
 * Implements hook_context_registry().
 */
function views_isotope_context_registry() {
  return [
    'reactions' => [
      'isotope' => [
        'title' => t('Isotope'),
        'description' => t('Set an Isotope configuration.'),
        'plugin' => 'IsotopeReaction',
      ],
    ],
  ];
}

/**
 * Return a list of available configs.
 *
 * @return array
 *   A list of available configs in a format ready for a select element.
 */
function views_isotope_available_configs() {
  $available_configs = ctools_export_crud_load_all('isotope_configurations');
  foreach ($available_configs as $key => $value) {
    $available_configs[$key] = $value->admin_title;
  }
  return $available_configs;
}

/**
 * Convert the name of a config into a JSON representation.
 *
 * Style as per http://isotope.metafizzy.co/#initialize-in-html.
 */
function views_isotope_get_config_json($config, array $additional_attributes) {
  ctools_include('export');
  $plugin = ctools_export_crud_load('isotope_configurations', $config);

  // Properties that should be passed directly to isotope.
  $allowed_attributes = [
    'layoutMode',
    'transitionDuration',
    'urlFilters',
    'isFitWidth',
    'isHorizontal',
    'stamp',
    'horizontalAlignment',
    'verticalAlignment',
    'isOriginLeft',
  ];

  // Default attributes.
  $attributes = [
    'columnWidth' => '.isotope-grid-sizer',
    'itemSelector' => '.isotope-element',
    'gutter' => '.isotope-gutter-sizer',
  ] + $additional_attributes;
  foreach ($plugin as $key => $value) {
    if (in_array($key, $allowed_attributes)) {
      $attributes[$key] = $value;
    }
  }
  return drupal_json_encode($attributes);
}

/**
 * Check for the library and return the appropriate scope.
 */
function views_isotope_check_library() {

  // @TODO: Cache the result.
  if (function_exists('libraries_get_path') && file_exists(libraries_get_path('isotope') . '/' . VIEWS_ISOTOPE_FILENAME) === TRUE) {

    // Library has been installed correctly, so use it.
    return 'library';
  }
  if (views_isotope_check_url(VIEWS_ISOTOPE_CDN_PATH)) {

    // Library has not been installed correctly, but the cdn is available.
    return 'cdn';
  }

  // Neither option is available. Throw an error.
  return FALSE;
}

/**
 * Return a list of accepted plugins.
 *
 * @return array
 *   Plugins accepted by this module.
 */
function views_isotope_additional_libraries_list() {
  $libraries['cellsByRow'] = [
    'filename' => 'cells-by-row.js',
    'type' => 'layout',
    'url' => 'https://raw.githubusercontent.com/metafizzy/isotope-cells-by-row/master/cells-by-row.js',
    'description' => t('A grid layout where items are centered inside each cell.'),
  ];
  $libraries['packery'] = [
    'filename' => 'packery-mode.pkgd.min.js',
    'type' => 'layout',
    'url' => 'https://raw.githubusercontent.com/metafizzy/isotope-packery/master/packery-mode.pkgd.min.js',
    'description' => t('The packery layout mode uses a bin-packing algorithm. This is a fancy way of saying “it fills empty gaps.”'),
  ];
  $libraries['masonryHorizontal'] = [
    'filename' => 'masonry-horizontal.js',
    'type' => 'layout',
    'url' => 'https://raw.githubusercontent.com/metafizzy/isotope-masonry-horizontal/master/masonry-horizontal.js',
    'description' => t('masonryHorizontal is the horizontal version of masonry. It works by placing elements in optimal position based on available horizontal space.'),
  ];
  $libraries['fitColumns'] = [
    'filename' => 'fit-columns.js',
    'type' => 'layout',
    'url' => 'https://raw.githubusercontent.com/metafizzy/isotope-fit-columns/master/fit-columns.js',
    'description' => t('Items are arranged into columns. Columns progress horizontally. fitColumns is ideal for items that have the same width.'),
  ];
  $libraries['cellsByColumn'] = [
    'filename' => 'cells-by-column.js',
    'type' => 'layout',
    'url' => 'https://raw.githubusercontent.com/metafizzy/isotope-cells-by-column/master/cells-by-column.js',
    'description' => t('A horizontal grid layout where items are centered inside each cell.'),
  ];
  $libraries['horizontal'] = [
    'filename' => 'horizontal.js',
    'type' => 'layout',
    'url' => 'https://raw.githubusercontent.com/metafizzy/isotope-horizontal/master/horizontal.js',
    'description' => t('Items are arranged horizontally.'),
  ];
  $libraries['imagesLoaded'] = [
    'filename' => 'imagesloaded.pkgd.min.js',
    'type' => 'plugin',
    'url' => 'http://imagesloaded.desandro.com/imagesloaded.pkgd.min.js',
    'description' => t('Detect when images have been loaded. (Requires jQuery version 1.9 or later.)'),
  ];
  return $libraries;
}

/**
 * Check for all additional libraries.
 *
 * Return the ones that have been detected, or an empty array. If passed a
 * specific libname to check for, will return the path to the library or FALSE.
 */
function views_isotope_check_additional_libraries($libname = FALSE) {
  $detected_libraries = [];
  $libraries = views_isotope_additional_libraries_list();
  if (function_exists('libraries_get_path')) {
    foreach ($libraries as $lib_name => $library) {

      // Check both for files inside the "isotope" folder and files inside a
      // plugin-named folder.
      $layout_path = libraries_get_path('isotope') . '/' . $library['filename'];
      if (file_exists($layout_path) === TRUE) {
        $detected_libraries[$layout_path] = $lib_name;
      }
      $plugin_path = libraries_get_path($lib_name) . '/' . $library['filename'];
      if (file_exists($plugin_path) === TRUE) {
        $detected_libraries[$plugin_path] = $lib_name;
      }
    }
  }
  if ($libname && in_array($libname, $detected_libraries)) {
    return array_search($libname, $detected_libraries);
  }
  elseif ($libname) {
    return FALSE;
  }
  return $detected_libraries;
}

/**
 * Function to add the right version of the js file.
 */
function views_isotope_addjs($config_name) {
  $isotope_scope = views_isotope_check_library();
  if ($isotope_scope == 'library') {
    drupal_add_js(libraries_get_path('isotope') . '/' . VIEWS_ISOTOPE_FILENAME);
  }
  elseif ($isotope_scope == 'cdn') {
    drupal_add_js(VIEWS_ISOTOPE_CDN_PATH, 'external');
  }
  ctools_include('export');
  $config = ctools_export_crud_load('isotope_configurations', $config_name);
  $libraries = views_isotope_additional_libraries_list();
  $selected_libraries = [
    $config->layoutMode,
  ];
  foreach ($config->plugins as $value) {
    if (!empty($value)) {
      $selected_libraries[] = $value;
    }
  }
  foreach ($selected_libraries as $lib_name) {
    if (!empty($libraries[$lib_name])) {
      $path = views_isotope_check_additional_libraries($lib_name);
      if (!empty($path)) {
        drupal_add_js($path);
      }
    }
  }
}

/**
 * Helper function to check if a url exists.
 */
function views_isotope_check_url($url) {
  $header_response = get_headers($url, 1);
  if (strpos($header_response[0], "200") === FALSE) {
    return FALSE;
  }
  return TRUE;
}

/**
 * Strip all special chars to make value suitable for css class name.
 *
 * @param string $raw
 *   The raw value to be sanitized.
 *
 * @return string
 *   The sanitized value.
 */
function views_isotope_sanitize($raw) {
  $safe = $raw;
  if (is_array($raw)) {
    $safe = [];
    foreach ($raw as $i) {
      $safe[] = views_isotope_sanitize($i);
    }
    return implode(' ', $safe);
  }

  // Transliterate other language chars to latin.
  if (function_exists('transliteration_get')) {
    $safe = transliteration_get($safe, '?', language_default('language'));
  }

  // Basic class-name rules.
  $safe = strtolower($safe);
  $safe = preg_replace('/[^a-z0-9]/s', '-', $safe);
  $safe = preg_replace('/-{2,}/', '-', $safe);

  // Allow other modules to modify it with hook_views_isotope_sanitize_alter().
  drupal_alter('views_isotope_sanitize', $safe, $raw);
  return $safe;
}

/**
 * Implements hook_theme().
 *
 * Defines the theming capabilities provided by this module.
 *
 * A default 'instance' of NULL means an optionset should apply to all instances
 * on the page, unless restricted to one or more.
 */
function views_isotope_theme($existing, $type, $theme, $path) {
  return [
    'isotope_grid' => [
      'variables' => [
        'config' => 'isotope_default_config',
        'items' => [],
        'instance' => NULL,
      ],
    ],
    'isotope_filter' => [
      'variables' => [
        'items' => [],
        'instance' => NULL,
        'filtername' => 'filter',
        'filtertitle' => NULL,
      ],
    ],
    'isotope_sorter' => [
      'variables' => [
        'sorts' => [],
        'original' => NULL,
        'instance' => NULL,
      ],
    ],
  ];
}

/**
 * Default theme implementation for the grid.
 *
 * @param array $vars
 *   Variables for theming.
 *
 * @return string
 *   Markup.
 */
function theme_isotope_grid(array $vars) {

  // A config name specified in context will override whatever we have been
  // passed.
  $config_name = $vars['config'];
  if (module_exists('context')) {
    if ($plugin = context_get_plugin('reaction', 'isotope')) {
      $context_name = $plugin
        ->execute();
      if (!empty($context_name)) {
        $config_name = $context_name;
      }
    }
  }

  // Add the sorting options to the initial configuration.
  $additional_config = [
    'getSortData' => [],
  ];
  foreach ($vars['items'] as $item) {
    $item['data'] = !empty($item['data']) ? $item['data'] : [];
    foreach ($item['data'] as $key => $value) {
      $additional_config['getSortData'][$key] = '.' . $key;
    }
  }

  // Retrieve the desired configuration (plus sorting options).
  $config = views_isotope_get_config_json($config_name, $additional_config);

  // Make sure the instance name is unique per page load.
  $global_instances =& drupal_static(__FUNCTION__);
  $global_instances = isset($global_instances) ? $global_instances : [];
  if (!empty($vars['instance']) && !in_array($vars['instance'], $global_instances)) {
    $instance_name = $vars['instance'];
  }
  else {
    for ($i = 0; $i >= 0; $i++) {
      if (!in_array($i, $global_instances)) {
        $instance_name = $i;

        // Break the infinite loop when successful.
        break;
      }
    }
  }
  $global_instances[] = $instance_name;
  $instance = 'isotope-instance-' . $instance_name;
  $items = [
    [
      'data' => '',
      'class' => [
        'isotope-grid-sizer',
      ],
    ],
    [
      'data' => '',
      'class' => [
        'isotope-gutter-sizer',
      ],
    ],
  ];
  foreach ($vars['items'] as $item) {
    $sorts = '';
    $item['data'] = !empty($item['data']) ? $item['data'] : [];
    foreach ($item['data'] as $key => $value) {
      if (!is_array($value)) {
        $value = [
          $value,
        ];
      }
      foreach ($value as $sort) {
        $sorts .= '<div class="sort-data ' . $key . '">' . views_isotope_sanitize($sort) . '</div>';
      }
      $item['data'][$key] = views_isotope_sanitize($value);
    }
    $classes = array_values($item['data']);
    $classes[] = 'isotope-element';
    $items[] = [
      'data' => $item['value'] . $sorts,
      'class' => $classes,
    ];
  }
  $return = [
    '#theme' => 'item_list',
    '#items' => $items,
    '#type' => 'ul',
    '#attributes' => [
      'class' => 'isotope-container js-isotope',
      'id' => $instance,
      'data-isotope-options' => $config,
    ],
    '#attached' => [
      'js' => [
        drupal_get_path('module', 'views_isotope') . '/views_isotope.js',
      ],
      'css' => [
        drupal_get_path('module', 'views_isotope') . '/views_isotope.css',
      ],
    ],
  ];
  views_isotope_addjs($config_name);
  return drupal_render($return);
}

/**
 * Default theme implementation for the filter list.
 *
 * @param array $vars
 *   Variables for theming.
 *
 * @return string
 *   Markup.
 */
function theme_isotope_filter(array $vars) {
  $multi_field_logic = 'OR';
  $attributes['class'] = 'isotope-options clearfix';
  if (!empty($vars['instance'])) {
    $attributes['data-instance-id'] = 'isotope-instance-' . $vars['instance'];
  }
  if (!empty($vars['filtername'])) {
    $attributes['data-filter-group'] = $vars['filtername'];
  }
  else {
    $attributes['data-filter-group'] = 'unnamed_filter';
  }
  $title = !empty($vars['filtertitle']) ? $vars['filtertitle'] : NULL;
  $items[] = l(t('All'), '', [
    'attributes' => [
      'class' => 'filterbutton',
      'data-filter' => '',
    ],
    'fragment' => 'filter',
    'external' => TRUE,
  ]);
  foreach ($vars['items'] as $key => $label) {
    $keys = explode(',', $key);
    foreach ($keys as $k => $v) {
      $keys[$k] = '.' . views_isotope_sanitize($v);
    }
    if ($multi_field_logic == 'OR') {
      $keys = implode(', ', $keys);
    }
    else {
      $keys = implode('', $keys);
    }
    $items[] = l($label, '', [
      'attributes' => [
        'class' => 'filterbutton',
        'data-filter' => $keys,
      ],
      'fragment' => 'filter',
      'external' => TRUE,
      'html' => TRUE,
    ]);
  }
  $return = [
    '#theme' => 'item_list',
    '#items' => $items,
    '#type' => 'ul',
    '#title' => $title,
    '#attributes' => $attributes,
  ];
  return drupal_render($return);
}

/**
 * Default theme implementation for the sorting list.
 *
 * @param array $vars
 *   Variables for theming.
 *
 * @return string
 *   Markup.
 */
function theme_isotope_sorter(array $vars) {
  $attributes['class'] = 'isotope-options sorts clearfix';
  if (!empty($vars['instance'])) {
    $attributes['data-instance-id'] = 'isotope-instance-' . $vars['instance'];
  }
  if (!empty($vars['original'])) {
    $vars['sorts'] = [
      $vars['original'] => 'original-order',
    ] + $vars['sorts'];
  }
  foreach ($vars['sorts'] as $key => $value) {
    $sort = is_array($value) ? implode(',', $value) : $value;
    $label = empty($key) || is_numeric($key) ? $sort : $key;
    $items[] = l($label, '', [
      'attributes' => [
        'class' => 'sorterbutton',
        'data-sort-by' => $sort,
      ],
      'fragment' => 'sorter',
      'external' => TRUE,
      'html' => TRUE,
    ]);
  }
  $return = [
    '#theme' => 'item_list',
    '#items' => $items,
    '#type' => 'ul',
    '#title' => t('Sort By'),
    '#attributes' => $attributes,
  ];
  return drupal_render($return);
}

Functions

Namesort descending Description
theme_isotope_filter Default theme implementation for the filter list.
theme_isotope_grid Default theme implementation for the grid.
theme_isotope_sorter Default theme implementation for the sorting list.
views_isotope_additional_libraries_list Return a list of accepted plugins.
views_isotope_addjs Function to add the right version of the js file.
views_isotope_available_configs Return a list of available configs.
views_isotope_check_additional_libraries Check for all additional libraries.
views_isotope_check_library Check for the library and return the appropriate scope.
views_isotope_check_url Helper function to check if a url exists.
views_isotope_context_plugins Implements hook_context_plugins().
views_isotope_context_registry Implements hook_context_registry().
views_isotope_ctools_plugin_api Implements hook_ctools_plugin_api().
views_isotope_ctools_plugin_directory Implements hook_ctools_plugin_directory().
views_isotope_default_isotope_configuration Implements hook_default_views_isotope_preset().
views_isotope_get_config_json Convert the name of a config into a JSON representation.
views_isotope_permission Implements hook_permission().
views_isotope_sanitize Strip all special chars to make value suitable for css class name.
views_isotope_theme Implements hook_theme().

Constants

Namesort descending Description
VIEWS_ISOTOPE_CDN_PATH @file Load the isotope library and provide configuration and theme options.
VIEWS_ISOTOPE_FILENAME