You are here

responsive_preview.module in Responsive Theme Preview 7

Same filename and directory in other branches
  1. 8 responsive_preview.module

Provides a component that previews the a page in various device dimensions.

File

responsive_preview.module
View source
<?php

/**
 * @file
 * Provides a component that previews the a page in various device dimensions.
 */

/**
 * Implements hook_help().
 */
function responsive_preview_help($path, $arg) {
  switch ($path) {
    case 'admin/help#responsive_preview':
      $output = '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Responsive Preview module provides a quick way to preview a page on your site within the dimensions of many popular device and screen sizes.') . '</p>';
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<p>' . t('To launch a preview, first click the navbar tab with the small device icon. The tab has the title "@title". A list of devices will appear. Selecting a device name will launch a preview of the current page within the dimensions of that device.', array(
        '@title' => t('Preview page layout'),
      )) . '</p>';
      $output .= '<p>' . t('To close the preview, click the close button signified visually by an x.') . '</p>';
      return $output;
  }
}

/**
 * Implements hook_menu().
 */
function responsive_preview_menu() {
  $items['admin/config/content/responsive-preview'] = array(
    'title' => 'Responsive preview',
    'description' => "Configure responsive preview device definitions.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'responsive_preview_admin_form',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'responsive_preview.admin.inc',
  );
  $items['admin/config/content/responsive-preview/add'] = array(
    'title' => 'Add device',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'responsive_preview_device_add_form',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_LOCAL_ACTION,
    'file' => 'responsive_preview.admin.inc',
  );
  $items['admin/config/content/responsive-preview/%'] = array(
    'title' => 'Edit',
    'title callback' => 'responsive_preview_title_callback',
    'title arguments' => array(
      4,
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'responsive_preview_device_edit_form',
      4,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'responsive_preview.admin.inc',
  );
  $items['admin/config/content/responsive-preview/%/edit'] = array(
    'title' => 'Edit',
    'title callback' => 'responsive_preview_title_callback',
    'title arguments' => array(
      4,
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/config/content/responsive-preview/%/delete'] = array(
    'title' => 'Delete',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'responsive_preview_device_delete_form',
      4,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'responsive_preview.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_theme().
 */
function responsive_preview_theme() {
  return array(
    'responsive_preview_admin_form' => array(
      'template' => 'templates/responsive-preview-admin-form',
      'file' => 'responsive_preview.admin.inc',
      'render element' => 'form',
    ),
  );
}

/**
 * Returns a list of devices and their properties from configuration.
 */
function responsive_preview_get_devices_list() {
  $devices = array();
  try {
    $devices = db_select('responsive_preview', 'rp')
      ->fields('rp')
      ->condition('status', 1)
      ->orderBy('weight', 'ASC')
      ->execute()
      ->fetchAllAssoc('name', PDO::FETCH_ASSOC);
  } catch (Exception $e) {
    watchdog_exception('responsive_preview', $e);
    throw $e;
  }
  $links = array();
  foreach ($devices as $name => $info) {
    $item = array(
      '#theme' => 'html_tag',
      '#tag' => 'button',
      '#value' => $info['label'],
      '#attributes' => array(
        'class' => array(
          'responsive-preview-device',
          'responsive-preview-icon',
          'responsive-preview-icon-active',
        ),
        'data-responsive-preview-name' => $name,
        'data-responsive-preview-width' => !empty($info['width']) ? $info['width'] : '',
        'data-responsive-preview-height' => !empty($info['height']) ? $info['height'] : '',
        'data-responsive-preview-dppx' => !empty($info['dppx']) ? $info['dppx'] : '1',
      ),
    );
    $links[$name] = array(
      // theme_item_list() doesn't work in D7 like it does in D8. You have to render items before
      // passing them in.
      'data' => drupal_render($item),
    );
  }

  // Add a configuration link.
  $configlink = array(
    '#theme' => 'link',
    '#text' => t('Configure devices'),
    '#path' => 'admin/config/content/responsive-preview',
    '#options' => array(
      'attributes' => array(
        'class' => array(
          'responsive-preview-configure',
        ),
      ),
      'html' => FALSE,
    ),
  );
  $links['configure_link'] = array(
    'data' => drupal_render($configlink),
  );
  return $links;
}

/**
 * Prevents the preview tab from rendering on administration pages.
 */
function responsive_preview_access() {
  return !path_is_admin(current_path());
}

/**
 * Implements hook_block_info().
 */
function responsive_preview_block_info() {
  $blocks['controls'] = array(
    'info' => t('Responsive page preview controls'),
    'properties' => array(
      'administrative' => FALSE,
    ),
  );
  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function responsive_preview_block_view($delta = '') {
  $block = array();
  if ($delta = 'controls') {
    $block['subject'] = t('Device preview');
    $block['content'] = array(
      'device_options' => array(
        '#cache' => array(
          'cid' => 'responsive_preview_device_options',
          'bin' => 'cache',
        ),
        '#theme' => 'item_list',
        '#items' => responsive_preview_get_devices_list(),
        '#attributes' => array(
          'class' => array(
            'responsive-preview-options',
          ),
        ),
        '#attached' => array(
          'library' => array(
            array(
              'responsive_preview',
              'responsive-preview',
            ),
          ),
        ),
      ),
    );
  }
  return $block;
}

/**
 * Implements hook_navbar().
 */
function responsive_preview_navbar() {
  $items['responsive_preview'] = array(
    '#type' => 'navbar_item',
    'tab' => array(
      'trigger' => array(
        '#theme' => 'html_tag',
        '#tag' => 'button',
        '#value' => t('Layout preview'),
        '#value_prefix' => '<span class="element-invisible">',
        '#value_suffix' => '</span>',
        '#attributes' => array(
          'title' => t('Preview page layout'),
          'class' => array(
            'responsive-preview-icon',
            'responsive-preview-icon-responsive-preview',
            'responsive-preview-trigger',
          ),
        ),
      ),
      'device_options' => array(
        '#theme' => 'item_list',
        '#items' => responsive_preview_get_devices_list(),
        '#attributes' => array(
          'class' => array(
            'responsive-preview-options',
            'item-list',
          ),
        ),
      ),
    ),
    '#wrapper_attributes' => array(
      'id' => 'responsive-preview-navbar-tab',
      'class' => array(
        'navbar-tab-responsive-preview',
      ),
    ),
    '#attached' => array(
      'library' => array(
        array(
          'responsive_preview',
          'responsive-preview',
        ),
      ),
    ),
    '#weight' => 200,
    '#access' => responsive_preview_access(),
  );
  return $items;
}

/**
 * Implements hook_library().
 */
function responsive_preview_library() {
  $path = drupal_get_path('module', 'responsive_preview');
  $options = array(
    'scope' => 'footer',
    'defer' => TRUE,
  );
  $libraries['responsive-preview'] = array(
    'title' => 'Preview layouts',
    'version' => VERSION,
    'css' => array(
      $path . '/css/responsive-preview.base.css',
      $path . '/css/responsive-preview.theme.css',
      $path . '/css/responsive-preview.icons.css',
    ),
    'js' => array(
      // Monkey-patch in jQuery UI 1.10 Position at $.fn.position_responsive_preview.
      $path . '/js/jquery/ducktape.position.js' => $options,
      $path . '/js/responsive-preview.js' => $options,
    ),
    'dependencies' => array(
      array(
        'system',
        'jquery',
      ),
      array(
        'responsive_preview',
        'debounce',
      ),
      array(
        'responsive_preview',
        'displace',
      ),
      array(
        'responsive_preview',
        'underscore',
      ),
      array(
        'responsive_preview',
        'backbone',
      ),
    ),
  );

  // A utility function to avoid stampeding function invocations.
  $libraries['debounce'] = array(
    'title' => 'Debounce',
    'version' => VERSION,
    'js' => array(
      $path . '/js/debounce.js' => array(
        'group' => JS_LIBRARY,
        'weight' => -19,
      ),
    ),
  );

  // A utility function determine viewport offset distances.
  $libraries['displace'] = array(
    'title' => 'Responsive preview displace',
    'version' => VERSION,
    'js' => array(
      $path . '/js/displace.js' => array(
        'group' => JS_LIBRARY,
      ),
    ),
    'dependencies' => array(
      array(
        'system',
        'jquery',
      ),
      array(
        'responsive_preview',
        'debounce',
      ),
    ),
  );

  // Underscore
  $libraries['underscore'] = _responsive_preview_convert_libraries_to_library(libraries_detect('underscore'), array(
    'group' => JS_LIBRARY,
    'weight' => -20,
  ));

  // Backbone
  $libraries['backbone'] = _responsive_preview_convert_libraries_to_library(libraries_detect('backbone'), array(
    'group' => JS_LIBRARY,
    'weight' => -19,
  ));
  return $libraries;
}

/**
 * Implements hook_library_alter().
 *
 * Backport a couple of things from jQuery that are required.
 */
function responsive_preview_library_alter(&$libraries, $module) {
  $jquery_version =& drupal_static(__FUNCTION__, NULL);
  if ($module == 'system') {
    $jquery_version = $libraries['jquery']['version'];
  }
  if ($jquery_version && $module == 'responsive_preview') {
    $path = drupal_get_path('module', 'responsive_preview');

    // If the version of jQuery is old, we need to add `on` and `off`.
    if ($jquery_version < '1.7') {
      $libraries['responsive-preview']['js'][$path . '/js/jquery/ducktape.events.js'] = array(
        'group' => JS_LIBRARY,
      );
    }
  }
}

/**
 * Implements hook_libraries_info().
 *
 * @see Libraries module.
 */
function responsive_preview_libraries_info() {
  $libraries = array();
  $common = array(
    'version callback' => '_responsive_preview_libraries_get_version',
    'variant order' => array(
      'minified',
      'source',
    ),
  );
  $libraries['underscore'] = array(
    'name' => 'Underscore',
    'vendor url' => 'http://documentcloud.github.io/backbone/',
    'download url' => 'https://github.com/jashkenas/underscore/archive/1.5.2.zip',
    'version arguments' => array(
      'variants' => array(
        'source' => array(
          'file' => 'underscore.js',
          // @todo Document an actual example version string.
          'pattern' => '#VERSION *\\W *[\'\\"]{1}(.*?)[\'\\"]{1}#',
          // In the unminified Underscore.js 1.5.2, the version is defined on
          // line 68.
          'lines' => 100,
        ),
        'minified' => array(
          'file' => 'underscore-min.js',
          'pattern' => '#VERSION *\\W *[\'\\"]{1}(.*?)[\'\\"]{1}#',
        ),
      ),
    ),
    'versions' => array(
      // Means ">=1.5.0": matches 1.5.0, 1.5.2, etc.
      '1.5.0' => array(
        'variants' => array(
          'source' => array(
            'name' => 'Underscore',
            'files' => array(
              'js' => array(
                'underscore.js',
              ),
            ),
            // Without a variant callback, the variant is assumed to be
            // installed.
            'variant callback' => '_responsive_preview_libraries_variant_exists',
            'variant arguments' => array(
              'underscore.js',
            ),
          ),
          'minified' => array(
            'name' => 'Underscore',
            'files' => array(
              'js' => array(
                'underscore-min.js',
              ),
            ),
            // Without a variant callback, the variant is assumed to be
            // installed.
            'variant callback' => '_responsive_preview_libraries_variant_exists',
            'variant arguments' => array(
              'underscore-min.js',
            ),
          ),
        ),
      ),
    ),
  );
  $libraries['underscore'] += $common;
  $libraries['backbone'] = array(
    'name' => 'Backbone',
    'vendor url' => 'http://documentcloud.github.io/backbone/',
    'download url' => 'https://github.com/jashkenas/backbone/archive/1.1.0.zip',
    'version arguments' => array(
      'variants' => array(
        'source' => array(
          'file' => 'backbone.js',
          // @todo Document an actual example version string.
          'pattern' => '#VERSION *\\W *[\'\\"]{1}(.*?)[\'\\"]{1}#',
          // In the unminified Backbone.js 1.1.0, the version is defined on line
          // 38.
          'lines' => 50,
        ),
        'minified' => array(
          'file' => 'backbone-min.js',
          'pattern' => '#VERSION *\\W *[\'\\"]{1}(.*?)[\'\\"]{1}#',
        ),
      ),
    ),
    'versions' => array(
      // Means ">=1.0.0": matches 1.0.0, 1.1.0, etc.
      '1.0.0' => array(
        'variants' => array(
          'source' => array(
            'name' => 'Backbone',
            'files' => array(
              'js' => array(
                'backbone.js',
              ),
            ),
            // Without a variant callback, the variant is assumed to be
            // installed.
            'variant callback' => '_responsive_preview_libraries_variant_exists',
            'variant arguments' => array(
              'backbone.js',
            ),
            'dependencies' => array(
              'underscore (>=1.5.0)',
            ),
          ),
          'minified' => array(
            'name' => 'Backbone',
            'files' => array(
              'js' => array(
                'backbone-min.js',
              ),
            ),
            // Without a variant callback, the variant is assumed to be
            // installed.
            'variant callback' => '_responsive_preview_libraries_variant_exists',
            'variant arguments' => array(
              'backbone-min.js',
            ),
            'dependencies' => array(
              'underscore (>=1.5.0)',
            ),
          ),
        ),
      ),
    ),
  );
  $libraries['backbone'] += $common;
  return $libraries;
}

/**
 * Determines the version of a responsive preview library.
 *
 * This is used in case different variants of the library are shipped separately
 * and, thus, different variants can contain different versions.
 *
 * @param array $library
 *   An associative array containing all information about the library. The
 *   library is assumed to have the following non-standard keys:
 *   - variant order: An array of variant names, ordered from the most preferred
 *     variant to the least preferred.
 * @param array $options
 *   An associative array with the following keys:
 *   - variants: An array of options for libraries_get_version() keyed by
 *     variant name.
 *
 */
function _responsive_preview_libraries_get_version(&$library, $options = array()) {
  $versions = array();
  foreach ($library['variant order'] as $variant_name) {
    $variant = $library['version arguments']['variants'][$variant_name];

    // Use the libraries get version function to determine the version string.
    $versions[$variant_name] = libraries_get_version($library, $variant);
  }

  // If no versions could be found for any of the variant, there is no version
  // to return. If different versions have been found, there is no way to
  // determine the correct one. We cannot use the information on the preferred
  // variants because we cannot guarantee that a less preferred variant will not
  // be loaded. Null values are fine. Either that variant file doesn't exist
  // or id doesn't contain version information. As long as the there is no
  // conflicting version information, the check should pass.
  $versions = array_filter($versions, '_responsive_preview_libraries_filter_null_values');
  $version = array_unique($versions);
  $vcount = count($version);
  if ($vcount == 1) {

    // A version number exists, so suppress any errors that any individual
    // variant might have raised.
    unset($library['error']);
    unset($library['error message']);
    return array_shift($version);
  }
  elseif ($vcount > 1) {
    $output = array();
    foreach ($versions as $name => $v) {
      $output[] = t('@name (@v)', array(
        '@name' => $name,
        '@v' => $v,
      ));
    }
    $library['error'] = 'inconsistent versions';
    $library['error message'] = t('The library\'s variants returned inconsistent versions: @variant_info', array(
      '@variant_info' => implode(', ', $output),
    ));
  }

  // If the version count is zero, then let the error from libraries_get_version
  // propagate through.
}

/**
 * Determines if an item is empty or not.
 *
 * @param string $item
 *   A version number string.
 * @return boolean
 *   Whether the $item's value is empty or not.
 */
function _responsive_preview_libraries_filter_null_values($item) {
  return !empty($item);
}

/**
 * Libraries API variant callback.
 */
function _responsive_preview_libraries_variant_exists($library, $variant_name, $required_file) {
  return file_exists($library['library path'] . '/' . $required_file);
}

/**
 * Converts a libraries module array to a hook_library array.
 *
 * What is this necessary? I don't see any way from the Libraries module API
 * to get an array that corresponds to what hook_library expects.
 */
function _responsive_preview_convert_libraries_to_library($library, $options = array()) {

  // If the library wasn't installed, don't bother converting it.
  if (!$library['installed']) {
    return array();
  }
  $converted = array();
  $files = array();

  // Get the library files from one of the installed variants.
  if ($name = _responsive_preview_libraries_get_preferred_variant_name($library)) {
    $files = $library['variants'][$name]['files'];
  }

  // Define the library if files exist for it.
  if (!empty($files)) {

    // This is the basic structure expected by hook_library().
    $converted = array(
      'title' => $library['name'],
      'website' => $library['vendor url'],
      'version' => $library['version'],
    );
    foreach ($files as $type => $paths) {
      foreach ($paths as $filename => $data) {
        $converted[$type][$library['library path'] . '/' . $filename] = $options;
      }
    }
  }
  return $converted;
}

/**
 * Returns the variant that should be loaded based on order preference.
 *
 * @param array $library
 *   A libraries module library definition array.
 * @return string
 *   The name of the variant that should be loaded.
 */
function _responsive_preview_libraries_get_preferred_variant_name($library) {
  if (!empty($library['variant order'])) {
    foreach ($library['variant order'] as $name) {
      if ($variant = $library['variants'][$name]) {
        if ($variant['installed']) {
          return $name;
        }
      }
    }
  }
  return NULL;
}

/**
 * Implements hook_testswarm_tests().
 */
function responsive_preview_testswarm_tests() {
  $path = drupal_get_path('module', 'responsive_preview');
  return array(
    'responsivePreview' => array(
      'module' => 'responsive_preview',
      'description' => 'Test the responsive preview module.',
      'js' => array(
        $path . '/tests/testswarm/responsive_preview.tests.js' => array(),
        array(
          'data' => array(
            'responsive_preview' => array(
              'devices' => config('responsive_preview.devices')
                ->get(),
            ),
          ),
          'type' => 'setting',
        ),
      ),
      'dependencies' => array(
        array(
          'system',
          'jquery',
        ),
        array(
          'system',
          'drupalSettings',
        ),
        array(
          'testswarm',
          'jquery.simulate',
        ),
      ),
      'path' => '',
      'permissions' => array(),
    ),
    'responsivePreviewAdmin' => array(
      'module' => 'responsive_preview',
      'description' => 'Test the responsive preview module admin.',
      'js' => array(
        $path . '/tests/testswarm/responsive_preview.admin.tests.js' => array(),
      ),
      'dependencies' => array(
        array(
          'system',
          'jquery',
        ),
      ),
      'path' => 'admin',
      'permissions' => array(),
    ),
  );
}

Functions

Namesort descending Description
responsive_preview_access Prevents the preview tab from rendering on administration pages.
responsive_preview_block_info Implements hook_block_info().
responsive_preview_block_view Implements hook_block_view().
responsive_preview_get_devices_list Returns a list of devices and their properties from configuration.
responsive_preview_help Implements hook_help().
responsive_preview_libraries_info Implements hook_libraries_info().
responsive_preview_library Implements hook_library().
responsive_preview_library_alter Implements hook_library_alter().
responsive_preview_menu Implements hook_menu().
responsive_preview_navbar Implements hook_navbar().
responsive_preview_testswarm_tests Implements hook_testswarm_tests().
responsive_preview_theme Implements hook_theme().
_responsive_preview_convert_libraries_to_library Converts a libraries module array to a hook_library array.
_responsive_preview_libraries_filter_null_values Determines if an item is empty or not.
_responsive_preview_libraries_get_preferred_variant_name Returns the variant that should be loaded based on order preference.
_responsive_preview_libraries_get_version Determines the version of a responsive preview library.
_responsive_preview_libraries_variant_exists Libraries API variant callback.