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"
],
"dependencies": {
"polymer": "Polymer/polymer#^1.1.0",
"angular-mixpanel": "~1.1.2"
}
}

View File

@ -4405,7 +4405,7 @@ a.badge:hover, a.badge:focus {
height: auto;
margin-left: auto;
margin-right: auto; }
.thumbnail .caption {
.thumbnail .caption, .thumbnail .icon-caption {
padding: 9px;
color: white; }
@ -5681,7 +5681,7 @@ button.close {
.clearfix:after {
clear: both; }
.center-block {
.center-block, .icon-caption {
display: block;
margin-left: auto;
margin-right: auto; }
@ -5997,7 +5997,7 @@ html {
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.caption {
.caption, .icon-caption {
font-weight: bold;
font-size: 11px;
margin-bottom: 0;
@ -6199,6 +6199,24 @@ button.btn:focus, button.progress-button:focus {
100% {
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
*
@ -6310,11 +6328,10 @@ button.btn:focus, button.progress-button:focus {
.alert-ribbon--open {
top: 0; }
hero-icon[disabled] .caption {
color: #787c7f; }
hero-icon[disabled] path {
fill: #787c7f; }
.icon-caption {
margin-top: 10px; }
.icon-caption[disabled] {
color: #787c7f; }
.block {
display: block; }
@ -6345,7 +6362,7 @@ body {
border-top: 2px solid #64686a; }
.section-footer .col-xs {
padding: 0; }
.section-footer hero-icon .icon {
.section-footer .svg-icon {
margin: 0 13px; }
.section-footer [os-open-external]:hover {
color: #85898c;

View File

@ -40,6 +40,7 @@ const app = angular.module('Etcher', [
// Components
require('./browser/components/progress-button/progress-button'),
require('./browser/components/drive-selector/drive-selector'),
require('./browser/components/svg-icon/svg-icon'),
// Pages
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() {
return {
restrict: 'A',
scope: {
osOpenExternal: '@'
},
link: function(scope, element) {
scope: false,
link: function(scope, element, attributes) {
// This directive might be added to elements
// other than buttons.
@ -64,10 +62,10 @@ module.exports = function() {
//
// See https://github.com/electron/electron/issues/5039
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/angular.css">
<link rel="import" href="components/hero-icon.html">
<script>
window._trackJs = {
token: '032448bc3d9e4cffb1e9b43d29d6c142',
@ -37,19 +35,19 @@
<main class="wrapper" ui-view></main>
<footer class="section-footer">
<hero-icon path="../assets/images/etcher.svg"
<svg-icon path="../../../../../assets/images/etcher.svg"
width="83px"
height="13px"
os-open-external="https://etcher.io"></hero-icon>
os-open-external="http://etcher.io"></svg-icon>
<span class="caption">
IS <span os-open-external="https://github.com/resin-io/etcher">AN OPEN SOURCE PROJECT</span> BY
</span>
<hero-icon path="../assets/images/resin.svg"
<svg-icon path="../../../../../assets/images/resin.svg"
width="79px"
height="23px"
os-open-external="https://resin.io"></hero-icon>
os-open-external="https://resin.io"></svg-icon>
</footer>
</body>
</html>

View File

@ -1,7 +1,8 @@
<div class="row around-xs">
<div class="col-xs">
<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>
<div class="space-vertical-large">
@ -25,7 +26,11 @@
<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>
<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>
@ -56,7 +61,12 @@
<div class="col-xs">
<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>
<div class="space-vertical-large">

View File

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