mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 07:17:18 +00:00
feat(GUI): rewrite svg-icon directive in react (#1464)
We change from using an Angular directive to using React through react2angular in the SVGIcon module.
This commit is contained in:
parent
ef739fb222
commit
a7d713c323
@ -55,7 +55,7 @@ const app = angular.module('Etcher', [
|
||||
require('./modules/drive-scanner'),
|
||||
|
||||
// Components
|
||||
require('./components/svg-icon/svg-icon'),
|
||||
require('./components/svg-icon'),
|
||||
require('./components/warning-modal/warning-modal'),
|
||||
require('./components/safe-webview'),
|
||||
|
||||
|
113
lib/gui/components/svg-icon.js
Normal file
113
lib/gui/components/svg-icon.js
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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 react = require('react');
|
||||
const propTypes = require('prop-types');
|
||||
const react2angular = require('react2angular').react2angular;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.SVGIcon';
|
||||
const angularSVGIcon = angular.module(MODULE_NAME, []);
|
||||
|
||||
const DEFAULT_SIZE = '40px';
|
||||
|
||||
/**
|
||||
* @summary SVG element that takes both filepaths and file contents
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
class SVGIcon extends react.Component {
|
||||
|
||||
/**
|
||||
* @summary Render the SVG
|
||||
* @returns {react.Element}
|
||||
*/
|
||||
render() {
|
||||
|
||||
// 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, this.props.path);
|
||||
|
||||
const contents = fs.readFileSync(imagePath, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
|
||||
const width = this.props.width || DEFAULT_SIZE;
|
||||
const height = this.props.height || DEFAULT_SIZE;
|
||||
|
||||
return react.createElement('div', {
|
||||
className: 'svg-icon',
|
||||
height,
|
||||
width,
|
||||
style: {
|
||||
width,
|
||||
height
|
||||
},
|
||||
disabled: this.props.disabled,
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: contents
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Cause a re-render due to changed element properties
|
||||
* @param {Object} nextProps - the new properties
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
// This will update the element if the properties change
|
||||
this.setState(nextProps);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SVGIcon.propTypes = {
|
||||
|
||||
/**
|
||||
* @summary SVG contents or path to the resource
|
||||
*/
|
||||
path: propTypes.string.isRequired,
|
||||
|
||||
/**
|
||||
* @summary SVG image width unit
|
||||
*/
|
||||
width: propTypes.string,
|
||||
|
||||
/**
|
||||
* @summary SVG image height unit
|
||||
*/
|
||||
height: propTypes.string,
|
||||
|
||||
/**
|
||||
* @summary Should the element visually appear grayed out and disabled?
|
||||
*/
|
||||
disabled: propTypes.bool
|
||||
|
||||
};
|
||||
|
||||
angularSVGIcon.component('svgIcon', react2angular(SVGIcon));
|
||||
module.exports = MODULE_NAME;
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* 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 _ = require('lodash');
|
||||
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.
|
||||
*
|
||||
* @returns {Object}
|
||||
*
|
||||
* @example
|
||||
* <svg-icon path="path/to/icon.svg" width="40px" height="40px"></svg-icon>
|
||||
*/
|
||||
module.exports = () => {
|
||||
return {
|
||||
templateUrl: './components/svg-icon/templates/svg-icon.tpl.html',
|
||||
replace: true,
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
path: '@',
|
||||
width: '@',
|
||||
height: '@'
|
||||
},
|
||||
link: (scope, element) => {
|
||||
element.css('width', scope.width || '40px');
|
||||
element.css('height', scope.height || '40px');
|
||||
|
||||
scope.$watch('path', (value) => {
|
||||
|
||||
// The path contains SVG contents
|
||||
if (_.first(value) === '<') {
|
||||
element.html(value);
|
||||
|
||||
} else {
|
||||
|
||||
// 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, value);
|
||||
|
||||
const contents = fs.readFileSync(imagePath, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
|
||||
element.html(contents);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
9
lib/gui/components/svg-icon/styles/_svg-icon.scss
Normal file
9
lib/gui/components/svg-icon/styles/_svg-icon.scss
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg-icon {
|
||||
display: inline-block;
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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;
|
@ -1 +0,0 @@
|
||||
<div class="svg-icon"></div>
|
@ -6413,6 +6413,13 @@ body {
|
||||
.modal-drive-selector-modal .word-keep {
|
||||
word-break: keep-all; }
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%; }
|
||||
|
||||
svg-icon {
|
||||
display: inline-block; }
|
||||
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
@ -6474,7 +6481,7 @@ body {
|
||||
.page-main {
|
||||
margin-top: 75px; }
|
||||
|
||||
.svg-icon[disabled] path {
|
||||
svg-icon > div[disabled] path {
|
||||
fill: #787c7f; }
|
||||
|
||||
.page-main .step-selection-text {
|
||||
@ -6693,6 +6700,8 @@ body {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%; }
|
||||
.section-footer > span[os-open-external] {
|
||||
display: flex; }
|
||||
|
||||
.section-loader webview {
|
||||
flex: 0 1;
|
||||
|
@ -32,19 +32,21 @@
|
||||
|
||||
<footer class="section-footer" ng-controller="StateController as state"
|
||||
ng-hide="state.currentName === 'success'">
|
||||
<svg-icon path="../../../assets/etcher.svg"
|
||||
width="83px"
|
||||
height="13px"
|
||||
os-open-external="https://etcher.io?ref=etcher_footer"></svg-icon>
|
||||
<span os-open-external="https://etcher.io?ref=etcher_footer">
|
||||
<svg-icon path="'../assets/etcher.svg'"
|
||||
width="'83px'"
|
||||
height="'13px'"></svg-icon>
|
||||
</span>
|
||||
|
||||
<span class="caption">
|
||||
is <span class="caption" os-open-external="https://github.com/resin-io/etcher">an open source project</span> by
|
||||
</span>
|
||||
|
||||
<svg-icon path="../../../assets/resin.svg"
|
||||
width="79px"
|
||||
height="23px"
|
||||
os-open-external="https://resin.io?ref=etcher"></svg-icon>
|
||||
<span os-open-external="https://resin.io?ref=etcher">
|
||||
<svg-icon path="'../assets/resin.svg'"
|
||||
width="'79px'"
|
||||
height="'23px'"></svg-icon>
|
||||
</span>
|
||||
|
||||
<span class="caption footer-right"
|
||||
manifest-bind="version"
|
||||
|
@ -15,21 +15,25 @@
|
||||
<div class="box center">
|
||||
<div class="fallback-banner">
|
||||
<div class="caption caption-big">Thanks for using
|
||||
<svg-icon path="../../../assets/etcher.svg"
|
||||
width="150px"
|
||||
height="auto"
|
||||
os-open-external="https://etcher.io?ref=etcher_offline_banner"></svg-icon>
|
||||
<span os-open-external="https://etcher.io?ref=etcher_offline_banner">
|
||||
<svg-icon path="'../assets/etcher.svg'"
|
||||
width="'150px'"
|
||||
height="'auto'">
|
||||
</svg-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="caption caption-small">
|
||||
made with
|
||||
<svg-icon path="../../../assets/love.svg"
|
||||
width="20px"
|
||||
height="auto"></svg-icon>
|
||||
<svg-icon path="'../assets/love.svg'"
|
||||
width="'20px'"
|
||||
height="'auto'"></svg-icon>
|
||||
by
|
||||
<svg-icon path="../../../assets/resin.svg"
|
||||
width="100px"
|
||||
height="auto"
|
||||
os-open-external="https://resin.io?ref=etcher_success"></svg-icon>
|
||||
<span os-open-external="https://resin.io?ref=etcher_success">
|
||||
<svg-icon path="'../assets/resin.svg'"
|
||||
width="'100px'"
|
||||
height="'auto'">
|
||||
</svg-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="section-footer">
|
||||
<span class="caption footer-right"
|
||||
|
@ -18,7 +18,7 @@
|
||||
margin-top: 75px;
|
||||
}
|
||||
|
||||
.svg-icon[disabled] path {
|
||||
svg-icon > div[disabled] path {
|
||||
fill: $palette-theme-dark-disabled-foreground;
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,9 @@
|
||||
<div class="col-xs" ng-controller="ImageSelectionController as image">
|
||||
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
|
||||
|
||||
<svg-icon
|
||||
class="center-block"
|
||||
path="{{ main.selection.getImageLogo() || '../../../assets/image.svg' }}"></svg-icon>
|
||||
<div class="center-block">
|
||||
<svg-icon path="main.selection.getImageLogo() || '../assets/image.svg'"></svg-icon>
|
||||
</div>
|
||||
|
||||
<div class="space-vertical-large">
|
||||
<div ng-hide="main.selection.hasImage()">
|
||||
@ -41,9 +41,10 @@
|
||||
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()"></div>
|
||||
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()"></div>
|
||||
|
||||
<svg-icon class="center-block"
|
||||
path="../../../assets/drive.svg"
|
||||
ng-disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
|
||||
<div class="center-block">
|
||||
<svg-icon path="'../assets/drive.svg'"
|
||||
disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
|
||||
</div>
|
||||
|
||||
<div class="space-vertical-large">
|
||||
<div ng-hide="main.selection.hasDrive()">
|
||||
@ -78,9 +79,10 @@
|
||||
|
||||
<div class="col-xs" ng-controller="FlashController as flash">
|
||||
<div class="box text-center">
|
||||
<svg-icon class="center-block"
|
||||
path="../../../assets/flash.svg"
|
||||
ng-disabled="main.shouldFlashStepBeDisabled()"></svg-icon>
|
||||
<div class="center-block">
|
||||
<svg-icon path="'../assets/flash.svg'"
|
||||
disabled="main.shouldFlashStepBeDisabled()"></svg-icon>
|
||||
</div>
|
||||
|
||||
<div class="space-vertical-large">
|
||||
<progress-button class="button-brick"
|
||||
|
@ -33,6 +33,7 @@ $link-color: #ddd;
|
||||
@import "../components/modal/styles/modal";
|
||||
@import "../components/progress-button/styles/progress-button";
|
||||
@import "../components/drive-selector/styles/drive-selector";
|
||||
@import "../components/svg-icon/styles/svg-icon";
|
||||
@import "../components/tooltip-modal/styles/tooltip-modal";
|
||||
@import "../components/warning-modal/styles/warning-modal";
|
||||
@import "../pages/main/styles/main";
|
||||
@ -83,6 +84,10 @@ body {
|
||||
right: 0;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
> span[os-open-external] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.section-loader {
|
||||
|
@ -10,7 +10,7 @@ require('angular-mocks');
|
||||
describe('Browser: SVGIcon', function() {
|
||||
|
||||
beforeEach(angular.mock.module(
|
||||
require('../../../lib/gui/components/svg-icon/svg-icon')
|
||||
require('../../../lib/gui/components/svg-icon')
|
||||
));
|
||||
|
||||
describe('svgIcon', function() {
|
||||
@ -18,22 +18,14 @@ describe('Browser: SVGIcon', function() {
|
||||
let $compile;
|
||||
let $rootScope;
|
||||
|
||||
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_, $templateCache) {
|
||||
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_) {
|
||||
$compile = _$compile_;
|
||||
$rootScope = _$rootScope_;
|
||||
|
||||
// Workaround `Unexpected request: GET template.html. No more request expected` error.
|
||||
// See http://stackoverflow.com/a/29437480/1641422
|
||||
const templatePath = './components/svg-icon/templates/svg-icon.tpl.html';
|
||||
const template = fs.readFileSync(path.resolve('lib', 'gui', templatePath), {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
$templateCache.put(templatePath, template);
|
||||
|
||||
}));
|
||||
|
||||
it('should inline the svg contents in the element', function() {
|
||||
const icon = '../../../../../lib/gui/assets/etcher.svg';
|
||||
const icon = '../../../lib/gui/assets/etcher.svg';
|
||||
let iconContents = _.split(fs.readFileSync(path.join(__dirname, '../../../lib/gui/assets/etcher.svg'), {
|
||||
encoding: 'utf8'
|
||||
}), /\r?\n/);
|
||||
@ -43,56 +35,33 @@ describe('Browser: SVGIcon', function() {
|
||||
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);
|
||||
const element = $compile(`<svg-icon path="'${icon}'">Resin.io</svg-icon>`)($rootScope);
|
||||
$rootScope.$digest();
|
||||
m.chai.expect(element.html()).to.equal(iconContents);
|
||||
});
|
||||
|
||||
it('should inline raw svg contents in the element', function() {
|
||||
const svg = '<svg><text>Raspbian</text></svg>';
|
||||
const element = $compile(`<svg-icon path="${svg}"></svg-icon>`)($rootScope);
|
||||
$rootScope.$digest();
|
||||
m.chai.expect(element.html()).to.equal(svg);
|
||||
});
|
||||
|
||||
it('should react to external updates', function() {
|
||||
const scope = $rootScope.$new();
|
||||
scope.name = 'Raspbian';
|
||||
scope.getSvg = () => {
|
||||
return `<svg><text>${scope.name}</text></svg>`;
|
||||
};
|
||||
|
||||
const element = $compile('<svg-icon path="{{ getSvg() }}"></svg-icon>')(scope);
|
||||
$rootScope.$digest();
|
||||
m.chai.expect(element.html()).to.equal('<svg><text>Raspbian</text></svg>');
|
||||
|
||||
scope.name = 'Resin.io';
|
||||
$rootScope.$digest();
|
||||
m.chai.expect(element.html()).to.equal('<svg><text>Resin.io</text></svg>');
|
||||
m.chai.expect(element.children().html()).to.equal(iconContents);
|
||||
});
|
||||
|
||||
it('should default the size to 40x40 pixels', function() {
|
||||
const icon = '../../../../../lib/gui/assets/etcher.svg';
|
||||
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);
|
||||
const icon = '../../../lib/gui/assets/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');
|
||||
m.chai.expect(element.children().css('width')).to.equal('40px');
|
||||
m.chai.expect(element.children().css('height')).to.equal('40px');
|
||||
});
|
||||
|
||||
it('should be able to set a custom width', function() {
|
||||
const icon = '../../../../../lib/gui/assets/etcher.svg';
|
||||
const element = $compile(`<svg-icon path="${icon}" width="20px">Resin.io</svg-icon>`)($rootScope);
|
||||
const icon = '../../../lib/gui/assets/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');
|
||||
m.chai.expect(element.children().css('width')).to.equal('20px');
|
||||
m.chai.expect(element.children().css('height')).to.equal('40px');
|
||||
});
|
||||
|
||||
it('should be able to set a custom height', function() {
|
||||
const icon = '../../../../../lib/gui/assets/etcher.svg';
|
||||
const element = $compile(`<svg-icon path="${icon}" height="20px">Resin.io</svg-icon>`)($rootScope);
|
||||
const icon = '../../../lib/gui/assets/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');
|
||||
m.chai.expect(element.children().css('width')).to.equal('40px');
|
||||
m.chai.expect(element.children().css('height')).to.equal('20px');
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user