Implement SVGIcon Angular directive (#324)

* Inherit current scope in osOpenExternal directive

This directive attempts to create a new isolated scope, which leads the
errors when using this directive on top of another directive in the same
element.

Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>

* Implement SVGIcon Angular directive

This directive replaces part of `hero-icon`, the old Polymer component.

Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
This commit is contained in:
Juan Cruz Viotti 2016-04-13 16:14:46 -04:00
parent 04efd16ca3
commit 0dcc7b22b8
13 changed files with 245 additions and 88 deletions

View File

@ -15,7 +15,6 @@
"tests" "tests"
], ],
"dependencies": { "dependencies": {
"polymer": "Polymer/polymer#^1.1.0",
"angular-mixpanel": "~1.1.2" "angular-mixpanel": "~1.1.2"
} }
} }

View File

@ -4405,7 +4405,7 @@ a.badge:hover, a.badge:focus {
height: auto; height: auto;
margin-left: auto; margin-left: auto;
margin-right: auto; } margin-right: auto; }
.thumbnail .caption { .thumbnail .caption, .thumbnail .icon-caption {
padding: 9px; padding: 9px;
color: white; } color: white; }
@ -5681,7 +5681,7 @@ button.close {
.clearfix:after { .clearfix:after {
clear: both; } clear: both; }
.center-block { .center-block, .icon-caption {
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; } margin-right: auto; }
@ -5997,7 +5997,7 @@ html {
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.caption { .caption, .icon-caption {
font-weight: bold; font-weight: bold;
font-size: 11px; font-size: 11px;
margin-bottom: 0; margin-bottom: 0;
@ -6199,6 +6199,24 @@ button.btn:focus, button.progress-button:focus {
100% { 100% {
background-position: 20px 20px; } } background-position: 20px 20px; } }
/*
* Copyright 2016 Resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.svg-icon[disabled] path {
fill: #787c7f; }
/* /*
* Copyright 2016 Resin.io * Copyright 2016 Resin.io
* *
@ -6310,11 +6328,10 @@ button.btn:focus, button.progress-button:focus {
.alert-ribbon--open { .alert-ribbon--open {
top: 0; } top: 0; }
hero-icon[disabled] .caption { .icon-caption {
color: #787c7f; } margin-top: 10px; }
.icon-caption[disabled] {
hero-icon[disabled] path { color: #787c7f; }
fill: #787c7f; }
.block { .block {
display: block; } display: block; }
@ -6345,7 +6362,7 @@ body {
border-top: 2px solid #64686a; } border-top: 2px solid #64686a; }
.section-footer .col-xs { .section-footer .col-xs {
padding: 0; } padding: 0; }
.section-footer hero-icon .icon { .section-footer .svg-icon {
margin: 0 13px; } margin: 0 13px; }
.section-footer [os-open-external]:hover { .section-footer [os-open-external]:hover {
color: #85898c; color: #85898c;

View File

@ -40,6 +40,7 @@ const app = angular.module('Etcher', [
// Components // Components
require('./browser/components/progress-button/progress-button'), require('./browser/components/progress-button/progress-button'),
require('./browser/components/drive-selector/drive-selector'), require('./browser/components/drive-selector/drive-selector'),
require('./browser/components/svg-icon/svg-icon'),
// Pages // Pages
require('./browser/pages/finish/finish'), require('./browser/pages/finish/finish'),

View File

@ -0,0 +1,63 @@
/*
* Copyright 2016 Resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const path = require('path');
const fs = require('fs');
/**
* @summary SVGIcon directive
* @function
* @public
*
* @description
* This directive provides an easy way to load SVG icons
* by embedding the SVG contents inside the element, making
* it possible to style icons with CSS.
*
* @example
* <svg-icon path="path/to/icon.svg" width="40px" height="40px"></svg-icon>
*/
module.exports = function() {
return {
templateUrl: './browser/components/svg-icon/templates/svg-icon.tpl.html',
replace: true,
restrict: 'E',
scope: {
path: '@',
width: '@',
height: '@'
},
link: function(scope, element) {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(__dirname, scope.path);
const contents = fs.readFileSync(imagePath, {
encoding: 'utf8'
});
element.html(contents);
element.css('width', scope.width || '40px');
element.css('height', scope.height || '40px');
}
};
};

View File

@ -0,0 +1,19 @@
/*
* Copyright 2016 Resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.svg-icon[disabled] path {
fill: $color-disabled;
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2016 Resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/**
* @module Etcher.Components.SVGIcon
*/
const angular = require('angular');
const MODULE_NAME = 'Etcher.Components.SVGIcon';
const SVGIcon = angular.module(MODULE_NAME, []);
SVGIcon.directive('svgIcon', require('./directives/svg-icon'));
module.exports = MODULE_NAME;

View File

@ -0,0 +1 @@
<div class="svg-icon"></div>

View File

@ -38,10 +38,8 @@ const nodeOpen = require('open');
module.exports = function() { module.exports = function() {
return { return {
restrict: 'A', restrict: 'A',
scope: { scope: false,
osOpenExternal: '@' link: function(scope, element, attributes) {
},
link: function(scope, element) {
// This directive might be added to elements // This directive might be added to elements
// other than buttons. // other than buttons.
@ -64,10 +62,10 @@ module.exports = function() {
// //
// See https://github.com/electron/electron/issues/5039 // See https://github.com/electron/electron/issues/5039
if (os.platform() === 'linux') { if (os.platform() === 'linux') {
return nodeOpen(scope.osOpenExternal); return nodeOpen(attributes.osOpenExternal);
} }
shell.openExternal(scope.osOpenExternal); shell.openExternal(attributes.osOpenExternal);
}); });
} }
}; };

View File

@ -1,57 +0,0 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<dom-module id="hero-icon">
<template>
<style>
:host ::content .caption {
display: block;
margin-top: 10px;
}
:host ::content .icon {
margin: 0 auto;
}
</style>
<div class="icon"></div>
<template is="dom-if" if="{{label}}">
<span class="caption">{{label}}</span>
</template>
</template>
<script>
const fs = require('fs');
const path = require('path');
Polymer({
is: 'hero-icon',
properties: {
path: {
type: String
},
label: {
type: String
},
width: {
type: String
},
height: {
type: String
}
},
ready: function() {
'use strict';
const iconElement = this.querySelector('.icon');
const imagePath = path.join(__dirname, this.path);
const contents = fs.readFileSync(imagePath, {
encoding: 'utf8'
});
iconElement.innerHTML = contents;
iconElement.style.width = this.width || '40px';
iconElement.style.height = this.height || '40px';
}
});
</script>
</dom-module>

View File

@ -7,8 +7,6 @@
<link rel="stylesheet" type="text/css" href="css/desktop.css"> <link rel="stylesheet" type="text/css" href="css/desktop.css">
<link rel="stylesheet" type="text/css" href="css/angular.css"> <link rel="stylesheet" type="text/css" href="css/angular.css">
<link rel="import" href="components/hero-icon.html">
<script> <script>
window._trackJs = { window._trackJs = {
token: '032448bc3d9e4cffb1e9b43d29d6c142', token: '032448bc3d9e4cffb1e9b43d29d6c142',
@ -37,19 +35,19 @@
<main class="wrapper" ui-view></main> <main class="wrapper" ui-view></main>
<footer class="section-footer"> <footer class="section-footer">
<hero-icon path="../assets/images/etcher.svg" <svg-icon path="../../../../../assets/images/etcher.svg"
width="83px" width="83px"
height="13px" height="13px"
os-open-external="https://etcher.io"></hero-icon> os-open-external="http://etcher.io"></svg-icon>
<span class="caption"> <span class="caption">
IS <span os-open-external="https://github.com/resin-io/etcher">AN OPEN SOURCE PROJECT</span> BY IS <span os-open-external="https://github.com/resin-io/etcher">AN OPEN SOURCE PROJECT</span> BY
</span> </span>
<hero-icon path="../assets/images/resin.svg" <svg-icon path="../../../../../assets/images/resin.svg"
width="79px" width="79px"
height="23px" height="23px"
os-open-external="https://resin.io"></hero-icon> os-open-external="https://resin.io"></svg-icon>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -1,7 +1,8 @@
<div class="row around-xs"> <div class="row around-xs">
<div class="col-xs"> <div class="col-xs">
<div class="box text-center" os-dropzone="app.selectImage($file)"> <div class="box text-center" os-dropzone="app.selectImage($file)">
<hero-icon path="../assets/images/image.svg" label="SELECT IMAGE"></hero-icon> <svg-icon class="center-block" path="../../../../../assets/images/image.svg"></svg-icon>
<span class="icon-caption">SELECT IMAGE</span>
<span class="badge space-top-medium">1</span> <span class="badge space-top-medium">1</span>
<div class="space-vertical-large"> <div class="space-vertical-large">
@ -25,7 +26,11 @@
<div class="step-border-left" ng-disabled="!app.selection.hasImage()"></div> <div class="step-border-left" ng-disabled="!app.selection.hasImage()"></div>
<div class="step-border-right" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()"></div> <div class="step-border-right" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()"></div>
<hero-icon path="../assets/images/drive.svg" ng-disabled="!app.selection.hasImage()" label="SELECT DRIVE"></hero-icon> <svg-icon class="center-block"
path="../../../../../assets/images/drive.svg"
ng-disabled="!app.selection.hasImage()"></svg-icon>
<span class="icon-caption"
ng-disabled="!app.selection.hasImage()">SELECT DRIVE</span>
<span class="badge space-top-medium" ng-disabled="!app.selection.hasImage()">2</span> <span class="badge space-top-medium" ng-disabled="!app.selection.hasImage()">2</span>
@ -56,7 +61,12 @@
<div class="col-xs"> <div class="col-xs">
<div class="box text-center"> <div class="box text-center">
<hero-icon path="../assets/images/flash.svg" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()" label="FLASH IMAGE"></hero-icon> <svg-icon class="center-block"
path="../../../../../assets/images/flash.svg"
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()"></svg-icon>
<span class="icon-caption"
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">FLASH IMAGE</span>
<span class="badge space-top-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</span> <span class="badge space-top-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</span>
<div class="space-vertical-large"> <div class="space-vertical-large">

View File

@ -42,15 +42,19 @@ $alert-padding: 13px;
@import "./components/button"; @import "./components/button";
@import "./components/tick"; @import "./components/tick";
@import "../browser/components/progress-button/styles/progress-button"; @import "../browser/components/progress-button/styles/progress-button";
@import "../browser/components/svg-icon/styles/svg-icon";
@import "./components/modal"; @import "./components/modal";
@import "./components/alert-ribbon"; @import "./components/alert-ribbon";
hero-icon[disabled] .caption { .icon-caption {
color: $color-disabled; @extend .caption;
} @extend .center-block;
hero-icon[disabled] path { margin-top: 10px;
fill: $color-disabled;
&[disabled] {
color: $color-disabled;
}
} }
.block { .block {
@ -95,7 +99,7 @@ body {
padding: 0; padding: 0;
} }
hero-icon .icon { .svg-icon {
margin: 0 13px; margin: 0 13px;
} }

View File

@ -0,0 +1,76 @@
'use strict';
const m = require('mochainon');
const fs = require('fs');
const path = require('path');
const angular = require('angular');
require('angular-mocks');
describe('Browser: SVGIcon', function() {
beforeEach(angular.mock.module(
require('../../../lib/browser/components/svg-icon/svg-icon')
));
describe('svgIcon', function() {
let $compile;
let $rootScope;
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_, $templateCache) {
$compile = _$compile_;
$rootScope = _$rootScope_;
// Workaround `Unexpected request: GET template.html. No more request expected` error.
// See http://stackoverflow.com/a/29437480/1641422
const templatePath = './browser/components/svg-icon/templates/svg-icon.tpl.html';
const template = fs.readFileSync(path.resolve('lib', templatePath), {
encoding: 'utf8'
});
$templateCache.put(templatePath, template);
}));
it('should inline the svg contents in the element', function() {
const icon = "../../../../../assets/images/etcher.svg";
let iconContents = fs.readFileSync(path.join(__dirname, '../../../assets/images/etcher.svg'), {
encoding: 'utf8'
}).split('\n');
// Injecting XML as HTML causes the XML header to be commented out.
// Modify here to ease assertions later on.
iconContents[0] = '<!--' + iconContents[0].slice(1, iconContents[0].length - 1) + '-->';
iconContents = iconContents.join('\n');
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.html()).to.equal(iconContents);
});
it('should default the size to 40x40 pixels', function() {
const icon = "../../../../../assets/images/etcher.svg";
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.css('width')).to.equal('40px');
m.chai.expect(element.css('height')).to.equal('40px');
});
it('should be able to set a custom height', function() {
const icon = "../../../../../assets/images/etcher.svg";
const element = $compile(`<svg-icon path="${icon}" width="20px">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.css('width')).to.equal('20px');
m.chai.expect(element.css('height')).to.equal('40px');
});
it('should be able to set a custom height', function() {
const icon = "../../../../../assets/images/etcher.svg";
const element = $compile(`<svg-icon path="${icon}" height="20px">Resin.io</svg-icon>`)($rootScope);
$rootScope.$digest();
m.chai.expect(element.css('width')).to.equal('40px');
m.chai.expect(element.css('height')).to.equal('20px');
});
});
});