Introducing Views Share

One of the technologies that made a lasting impression on me, as a young programmer, was Microsoft OLE. To give my own applications the ability to embed documents created in other applications, and vice-versa, was mind-blowing! But the even bigger thrill came when I understood how the API had been architected to achieve this.

Then came the Web and we had to rebuild everything from scratch - not a bad thing really, since we're still very new at this coding thing :-)

Document sharing and embedding is one of the cornerstones of the modern Web. That's why I was happy to accept a commission to write a new module that allows views to be shared - thanks herd45!. The result is Views Share, and I am proud to release version 1.0 today. Here are some interesting bits from the code:

Views Share dialog in action

General architecture

The module was originally inspired by the architecture of Share This Thing, which accomplishes a similar function for nodes. The functionality is packaged as a Views area handler, which allows to add a link to the view's header or footer - the link that opens up the sharing dialog. The dialog shows the view's original URL for sharing as well as an embed code for the view. The embed code is an IFRAME tag that points to a special URL that the module catches to render the embedded view.

Rendering the embedded view

The embedded view needs to be rendered in an undecorated page: no sidebars, header, footer, or any other Drupal component, except for the view itself. To do this, a new theme function is introduced. This theme renders a full page, complete with HEAD and BODY, and only incorporates the Drupal parts that are absolutely needed. Here's the code for the theme preprocessor and its template file:

function template_preprocess_views_share(&$variables) {
  global $base_url, $language;

  $variables['content'] = /* render the view here */
  $variables['title'] = $view->get_title();
  $variables['base_url'] = $base_url;
  $variables['language'] = $language;
  $variables['language_dir'] = $language->direction == LANGUAGE_RTL ? 'rtl' : 'ltr';
  $variables['head']     = drupal_get_html_head();
  $variables['styles']   = drupal_get_css();
  $variables['scripts']  = drupal_get_js();
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">
<html lang="<?php print $language->language; ?>" dir="<?php print $language_dir; ?>">
<head>
<?php print $head; ?>
  <title><?php print $title; ?></title>
  <base href="<?php print $base_url; ?>">
  <?php print $styles; ?>
  <?php print $scripts; ?>
</head>
<body class="views-share">

  <?php print $content; ?>

</body>
</html>

To prevent Drupal from rendering its own fully decorated pages, I call drupal_exit() right after printing the theme above.

Previewing the embedded view

One of the requirements of the module was to support embed previewing, based on the way Google Maps does the same: Previewing the view embedding

This was fun to implement, and there are a couple of interesting JavaScript tricks:

Opening a link in a new window

To emulate the Google Maps previewing model (and to accommodate an arbitrary IFRAME size), I needed to open the preview dialog in a different browser window. By listening to a link's click event, I was able to make it open a new window that I specified:

$('a.views-share-preview').live('click', function(e) {
    e.preventDefault();

    // Open popup in a new window center screen and listen to its messages.
    var width = 1024,
        height = 768,
        left  = ($(window).width()-width)/2,
        top   = ($(window).height()-height)/2,
        popup = window.open(
          $(this).attr('href'), 
          'views-share-preview',
          'scrollbars=1,width='+width+',height='+height+',top='+top+',left='+left
        );
});

Updating share dialog embed code with values from the preview window

Now when the user adjusts the embedding values in the preview window, I need to update the embed code in the underlying share dialog. window.postMessage is the API that allows browser windows to communicate. In my case, the preview window sends an update message to its opener:

      // http://tutorialzine.com/2013/07/quick-tip-parse-urls/
      var a = $('', { href:location.href } )[0];
      $('#edit-embed-width, #edit-embed-height').change(function() {
        var embedCode = Drupal.settings.viewsSharePreview.embedCode
          .replace('%width', $('#edit-embed-width').val())
          .replace('%height', $('#edit-embed-height').val());
        $('#edit-embed-share').val(embedCode); // update embed code
        $('iframe').replaceWith(embedCode); // update preview
        window.opener.postMessage({embedCode: embedCode}, a.baseURI); // update parent window
      });

The opener window (which contains the share dialog) receives and processes the new embed code:

    // http://stackoverflow.com/questions/8822907/html5-cross-browser-iframe-po...
    var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
    var eventWindow = window[eventMethod];
    var eventMessage = eventMethod == "attachEvent" ? "onmessage" : "message";
    eventWindow(eventMessage, function(event) {
      $('#edit-share-embed').val(event.data.embedCode);
    }, false);

Supporting oEmbed

Another fun bit was adding support for oEmbed. This was done in two parts: first, add the oEmbed discovery tags on the regular view page, and then respond to oEmbed calls on our endpoint.

Discovery

The oEmbed standard specifies that a page can make its oEmbed content discoverable, by adding LINK tags to its HEAD. This is what our Views area handler does in case the oEmbed option is chosen. The endpoints specified in the LINK tags are also handled by our module, just like in the regular embed case.

Rendering

An oEmbed structure is created by the module in response to an oEmbed request. Its most important payload is the embed code that can be used by the oEmbed consumer. The consumer can ask for the structure in JSON or XML format. To render JSON, drupal provides the drupal_json_output() function, but to convert an object to XML, I was able to find a simple class on PHPClasses.org that does the job:

// @see http://www.phpclasses.org/package/4657-PHP-Generate-XML-from-values-of-o...
if (!class_exists('ObjectToXML')) {
  class ObjectToXML {
     private $dom;

     public function __construct($obj) {
        $this->dom = new DOMDocument("1.0", "UTF8");
        $this->dom->xmlStandalone = true;
        $root = $this->dom->createElement(get_class($obj));
        foreach($obj as $key=>$value) {
          $node = $this->createNode($key, $value);
          if($node != NULL) $root->appendChild($node);
        }
        $this->dom->appendChild($root);
     }

    private function createNode($key, $value) {
        $node = NULL;
        if(is_string($value) || is_numeric($value) || is_bool($value) || $value == NULL) {
          if($value == NULL) $node = $this->dom->createElement($key);
        else $node = $this->dom->createElement($key, (string)$value);
        } else {
        $node = $this->dom->createElement($key);
        if($value != NULL) {
          foreach($value as $key=>$value) {
            $sub = $this->createNode($key, $value);
            if($sub != NULL)  $node->appendChild($sub);
          }
        }
        }
      return $node;
    }

    public function __toString() {
      return $this->dom->saveXML();
    }
  }
}

Conclusion

You can see a demo of Views Share in action on my Feeds+Views demo site. I had lots of fun building that module, now go on and use it to make your views shareable!

AttachmentSize
views-share-preview.png69.66 KB

Comments

Good idea, would be very useful when filtering large data sets and then wanting to share to that specific data grouping or embed it elsewhere. Something worth checking out that could complement this approach is http://drupal.org/project/entity_iframe . It's my way of doing something similar but for entity types.

Thanks for the reference, I will check it out!

Note: at the time of writing, the Drupal.org site has issues with creating new module releases. So you will need to get the code from git until this is resolved.