Skip to content

Commit

Permalink
feat(docs): edit code example in codepen
Browse files Browse the repository at this point in the history
*  add support for running specs in docs
*  create service that converts docs demo information to a new codepen.
   * all other demos use icons/images located in docs/app/img/icons or docs/app/img
*  asset cache is required to serve SVG images on codepen.
   *  This script allows collaborators to regenerate the assetMap JSON located in the asset-cache.js file.
*  add button to each example to open the example in codepen to edit.
*  ng-app attribute is appended to a parent element in the docs site, and is appended to the parent element of the index.html file when sending to codepen.
*  need to send full path to codepen in order to retrieve the asset.
*  use <code> block around code docs
*  register icon sets inside demo
*  .sample is not included in the example html sent to codepen.
*  add documentation for codepen usage
*  Conflicts with karma port defined for core material design
*  compress codepen logo
*  add comment to describe karma-docs
*  improve documentation of functions
*  use ng-src instead of src
*  document build-asset-cache.sh
*  add docs for building hidden form
*  remove aria-label from codepen button
*  hide examples that are not editable
  *  All the layout examples are not defined with js/css files.  This departs from how component examples work.
*  hide edit on codepen for small layout

Closes angular#2604. Closes angular#2757.
  • Loading branch information
matthewrfindley authored and ThomasBurleson committed May 7, 2015
1 parent 4de34f8 commit 5c37dc8
Show file tree
Hide file tree
Showing 47 changed files with 763 additions and 118 deletions.
53 changes: 53 additions & 0 deletions config/karma-docs.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Used for running unit tests against the docs site
// Unit tests can be run using gulp docs-karma
module.exports = function(config) {

var UNCOMPILED_SRC = [
'docs/spec/**/*.spec.js'
];

// releaseMode is a custom configuration option.
var testSrc = UNCOMPILED_SRC;
var dependencies = process.env.KARMA_TEST_JQUERY ?
['node_modules/jquery/dist/jquery.js'] : [];

dependencies = dependencies.concat([
'node_modules/angular/angular.js',
'node_modules/angular-animate/angular-animate.js',
'node_modules/angular-aria/angular-aria.js',
'node_modules/angular-messages/angular-messages.js',
'node_modules/angular-route/angular-route.js',
'node_modules/angular-mocks/angular-mocks.js',
'dist/angular-material.js',
'config/test-utils.js',
'dist/docs/docs.js',
'dist/docs/docs-demo-scripts.js'
]);

config.set({

basePath: __dirname + '/..',
frameworks: ['jasmine'],
files: dependencies.concat(testSrc),

port: 9877,
reporters: ['progress'],
colors: true,

// Continuous Integration mode
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
singleRun: false,

// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera (has to be installed with `npm install karma-opera-launcher`)
// - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
// - PhantomJS
// - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
browsers: ['Chrome']
});

};
55 changes: 55 additions & 0 deletions docs/app/asset-cache.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/app/img/icons/codepen-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
1 change: 1 addition & 0 deletions docs/app/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ function($rootScope) {
function($scope, $attrs, $location, $rootScope) {
$rootScope.currentComponent = $rootScope.currentDoc = null;

$scope.exampleNotEditable = true;
$scope.layoutDemo = {
mainAxis: 'center',
crossAxis: 'center',
Expand Down
157 changes: 157 additions & 0 deletions docs/app/js/codepen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
(function() {
DocsApp
.factory('codepenDataAdapter', CodepenDataAdapter)
.factory('codepen', ['$demoAngularScripts', '$document', 'codepenDataAdapter', Codepen]);

// Provides a service to open a code example in codepen.
function Codepen($demoAngularScripts, $document, codepenDataAdapter) {

var CODEPEN_API = 'http://codepen.io/pen/define/';

return {
editOnCodepen: editOnCodepen
};

// Creates a codepen from the given demo model by posting to Codepen's API
// using a hidden form. The hidden form is necessary to avoid a CORS issue.
// See http://blog.codepen.io/documentation/api/prefill
function editOnCodepen(demo) {
var data = codepenDataAdapter.translate(demo, $demoAngularScripts.all());
var form = buildForm(data);
$document.find('body').append(form);
form[0].submit();
form.remove();
}

// Builds a hidden form with data necessary to create a codepen.
function buildForm(data) {
var form = angular.element(
'<form style="display: none;" method="post" target="_blank" action="' +
CODEPEN_API +
'"></form>'
);
var input = '<input type="hidden" name="data" value="' + escapeJsonQuotes(data) + '" />';
form.append(input);
return form;
}

// Recommended by Codepen to escape quotes.
// See http://blog.codepen.io/documentation/api/prefill
function escapeJsonQuotes(json) {
return JSON.stringify(json)
.replace(/"/g, "&quot;")
.replace(/"/g, "&apos;");
}
}

// Translates demo metadata and files into Codepen's post form data. See api documentation for
// additional fields not used by this service. http://blog.codepen.io/documentation/api/prefill
function CodepenDataAdapter() {

var CORE_JS = 'https://cdn.rawgit.com/angular/bower-material/master/angular-material.js';
var CORE_CSS = 'https://cdn.rawgit.com/angular/bower-material/master/angular-material.css';
var ASSET_CACHE_JS = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/t-114/assets-cache.js';

return {
translate: translate
};

// Translates a demo model to match Codepen's post data
// See http://blog.codepen.io/documentation/api/prefill
function translate(demo, externalScripts) {
var files = demo.files;

return {
title: demo.title,
html: processHtml(demo),
css: mergeFiles(files.css).join(' '),
js: processJs(files.js),
js_external: externalScripts.concat([CORE_JS, ASSET_CACHE_JS]).join(';'),
css_external: CORE_CSS
};
}

// Modifies index.html with neccesary changes in order to display correctly in codepen
// See each processor to determine how each modifies the html
function processHtml(demo) {
var index = demo.files.index.contents;

var processors = [
applyAngularAttributesToParentElement,
insertTemplatesAsScriptTags,
htmlEscapeAmpersand
];

processors.forEach(function(processor) {
index = processor(index, demo);
});

return index;
}

// Applies modifications the javascript prior to sending to codepen.
// Currently merges js files and replaces the module with the Codepen
// module. See documentation for replaceDemoModuleWithCodepenModule.
function processJs(jsFiles) {
var mergedJs = mergeFiles(jsFiles).join(' ');
var script = replaceDemoModuleWithCodepenModule(mergedJs);
return script;
}

// Maps file contents to an array
function mergeFiles(files) {
return files.map(function(file) {
return file.contents;
});
}

// Adds class to parent element so that styles are applied correctly
// Adds ng-app attribute. This is the same module name provided in the asset-cache.js
function applyAngularAttributesToParentElement(html, demo) {
var tmp;

// Grab only the DIV for the demo...
angular.forEach(angular.element(html), function(it,key){
if ((it.nodeName != "SCRIPT") || (it.nodeName != "#text")) {
tmp = angular.element(it);
}
});

tmp.addClass(demo.id);
tmp.attr('ng-app', 'MyApp');
return tmp[0].outerHTML;
}

// Adds templates inline in the html, so that templates are cached in the example
function insertTemplatesAsScriptTags(indexHtml, demo) {
if (demo.files.html.length) {
var tmp = angular.element(indexHtml);
angular.forEach(demo.files.html, function(template) {
tmp.append("<script type='text/ng-template' id='" +
template.name + "'>" +
template.contents +
"</script>");
});
return tmp[0].outerHTML;
}
return indexHtml;
}

// Escapes ampersands so that after codepen unescapes the html the escaped code block
// uses the correct escaped characters
function htmlEscapeAmpersand(html) {
return html
.replace(/&gt;/g, "&amp;gt;")
.replace(/&lt;/g, "&amp;lt;");
}

// Required to make codepen work. Demos define their own module when running on the
// docs site. In order to ensure the codepen example can use the asset-cache, the
// module needs to match so that the $templateCache is populated with the necessary
// assets.
function replaceDemoModuleWithCodepenModule(file) {
var matchAngularModule = /\.module\(('[^']*'|"[^"]*")\s*,(?:\s*\[([^\]]*)\])?/g;
return file.replace(matchAngularModule, ".module('MyApp'");
}
}
})();
16 changes: 13 additions & 3 deletions docs/app/js/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ function($mdUtil) {
scope: true,
templateUrl: 'partials/docs-demo.tmpl.html',
transclude: true,
controller: ['$scope', '$element', '$attrs', '$interpolate', DocsDemoCtrl],
controller: ['$scope', '$element', '$attrs', '$interpolate', 'codepen', DocsDemoCtrl],
controllerAs: 'demoCtrl',
bindToController: true
};

function DocsDemoCtrl($scope, $element, $attrs, $interpolate) {
function DocsDemoCtrl($scope, $element, $attrs, $interpolate, codepen) {
var self = this;

self.interpolateCode = angular.isDefined($attrs.interpolateCode);
Expand Down Expand Up @@ -49,6 +49,16 @@ function($mdUtil) {
.concat(self.files.js || [])
.concat(self.files.css || [])
.concat(self.files.html || []);

};

self.editOnCodepen = function() {
codepen.editOnCodepen({
title: self.demoTitle,
files: self.files,
id: self.demoId,
module: self.demoModule
});
};

function convertName(name) {
Expand Down Expand Up @@ -77,7 +87,7 @@ function($mdUtil) {

return function postLink(scope, element, attr, docsDemoCtrl) {
docsDemoCtrl.addFile(
$interpolate(name)(scope),
$interpolate(name)(scope),
$q.when(scope.$eval(contentsAttr) || html)
);
element.remove();
Expand Down
26 changes: 26 additions & 0 deletions docs/app/js/scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
(function() {
DocsApp
.factory('$demoAngularScripts', ['BUILDCONFIG', DemoAngularScripts]);

function DemoAngularScripts(BUILDCONFIG) {
var scripts = [
'angular.js',
'angular-animate.min.js',
'angular-route.min.js',
'angular-aria.min.js',
'angular-messages.min.js'
];

return {
all: allAngularScripts
};

function allAngularScripts() {
return scripts.map(fullPathToScript);
};

function fullPathToScript(script) {
return "https://ajax.googleapis.com/ajax/libs/angularjs/" + BUILDCONFIG.ngVersion + "/" + script;
};
};
})();
4 changes: 4 additions & 0 deletions docs/app/partials/docs-demo.tmpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ <h3>{{demoCtrl.demoTitle}}</h3>
Source
</div>
</md-button>
<md-button ng-hide="{{exampleNotEditable}}" hide-sm ng-click="demoCtrl.editOnCodepen()">
<md-icon md-svg-src="img/icons/codepen-logo.svg"></md-icon>
Edit on codepen
</md-button>
</div>
</md-toolbar>

Expand Down
73 changes: 73 additions & 0 deletions docs/guides/CODEPEN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Editable Demos in Codepen

## Description

Users will be able to click a button on each demo to open in codepen
to edit. From there the user can edit, save or make other
modifications to the example.

## Why Codepen?

Codepen appears to be one the most stable and active online sandboxes.
It has less accessibility problems then some of the other tools.

## How does it work?

When the user clicks on the **'Edit on codepen'** button, all files including
html, css, js, templates are used to create the new codepen by posting
to the [Codepen API](http://blog.codepen.io/documentation/api/prefill/). An
additional script is appended to the example to initialize the
[cache](#asset_cache), which is responsible for serving assets.

## As a contributor, what do I need to know?

* [SVG images are served from a cache](#asset_cache)
* [Adding a new SVG requires a change to the asset cache](#build_cache)
* Anytime a new dependency is added to an example, the [asset-cache.js](../app/asset-cache.js)
will need to be updated with the new dependency and [uploaded to the
CDN](#update_cdn)
* Images used in demos must use full paths
* Code examples are modified prior to sending to codepen with the same
module defined in the [asset-cache.js](../app/asset-cache.js)
* Additional HTML template files located in the demo directory are appended to your index file using `ng-template`. [See docs](https://docs.angularjs.org/api/ng/directive/script)

## <a name="asset_cache"></a> Asset Cache

SVG images are stored in an asset cache using `$templateCache`. A
script is delivered to codepen that initializes the cache within the
demo module.

### Why is an asset cache needed for Codepen?

Components within angular material at times use icons or SVG. Images
are fetched over http. Without having a server that will allow cross
site scripting (`Access-Control-Allow-Origin: *`), the request will
fail with a [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)
error.

The asset cache is intended to bypass any http request for an image
and serve the cached content.

### <a name="build_cache"></a> How do I populate the cache?

* Make all changes necessary to add or update any svg images
* run `./scripts/build-asset-cache.sh | pbcopy` to add an object
literal to your paste buffer.
* paste object literal as `var assetMap = { ... }` in the
[asset-cache.js](../app/asset-cache.js)
* [update](#build_cdn) the CDN with the new script
* commit asset-cache.js

### <a name="update_cdn"></a> Update Codepen Asset Cache

CDN is located on the Codepen PRO account.

* Follow the [instructions](http://blog.codepen.io/documentation/pro-features/asset-hosting/#asset-manager) on how to update the script.
* NOTE: be sure to update the script. DO NOT upload a new script. The URL should remain the same

## Deployment Considerations

The step to generate and deploy the asset-cache.js is currently a
manual process. Keep in mind that if changes are made to
asset-cache.js then you will need to follow the [steps](#update_cdn)
to update the cache on the CDN.
Loading

0 comments on commit 5c37dc8

Please sign in to comment.