1
0
mirror of https://github.com/chylex/Firefox-SCsCC.git synced 2025-04-09 07:15:43 +02:00

rewrite options as hyperapp

This commit is contained in:
sarics 2018-03-12 09:35:39 +01:00
parent 6320ff5eed
commit 8689bc2f4a
13 changed files with 171 additions and 205 deletions

View File

@ -19,5 +19,8 @@ module.exports = {
'no-param-reassign': ['error', { props: false }],
'react/no-unknown-property': 'off',
'react/prop-types': 'off',
'jsx-a11y/label-has-for': 'off',
},
};

View File

@ -2,7 +2,7 @@ export default [
{
title: 'Convert to',
name: 'toCurr',
type: 'menulist',
type: 'select',
value: '',
options: [
{ value: '', label: 'Please select a currency' },
@ -175,13 +175,13 @@ export default [
{
title: 'Round price',
name: 'round',
type: 'bool',
type: 'checkbox',
value: true,
},
{
title: 'Currency symbol',
name: 'symbol',
type: 'string',
type: 'text',
value: '',
},
{
@ -203,13 +203,13 @@ export default [
{
title: 'Separate symbol from price',
name: 'symbSep',
type: 'bool',
type: 'checkbox',
value: true,
},
{
title: 'Thousand separator',
name: 'sepTho',
type: 'menulist',
type: 'select',
value: ' ',
options: [
{
@ -233,7 +233,7 @@ export default [
{
title: 'Decimal separator',
name: 'sepDec',
type: 'menulist',
type: 'select',
value: ',',
options: [
{
@ -249,13 +249,13 @@ export default [
{
title: 'Add custom style to converted prices',
name: 'style',
type: 'bool',
type: 'checkbox',
value: true,
},
{
title: 'Show exchange rate update notifications',
name: 'noti',
type: 'bool',
type: 'checkbox',
value: true,
},
];

13
src/options/App.jsx Normal file
View File

@ -0,0 +1,13 @@
import { h } from 'hyperapp';
import OptionsContainer from './components/OptionsContainer';
const App = ({ options, preferences }, { preferences: { handleChange } }) => (
<div class="container">
<h1>SCs Currency Converter - Options</h1>
<OptionsContainer options={options} preferences={preferences} onOptionChange={handleChange} />
</div>
);
export default App;

35
src/options/actions.js Normal file
View File

@ -0,0 +1,35 @@
const actions = {
preferences: {
handleChange: ({ type, name, value }) => (preferences) => {
const changedPrefs = {
[name]: type === 'text' ? value.trim() : value,
};
// change the symbol on currency change
if (name === 'toCurr') {
changedPrefs.symbol = value;
}
// if a space inserted before or after the symbol, remove it, and set symbSep to true
if (name === 'symbol' && value !== value.trim() && !preferences.symbSep) {
changedPrefs.symbSep = true;
}
// don't let to set the same thousand and decimal separator
if (name === 'sepTho' && preferences.sepDec === value) {
changedPrefs.sepDec = value === ',' ? '.' : ',';
}
if (name === 'sepDec' && preferences.sepTho === value) {
changedPrefs.sepTho = value === ',' ? '.' : ',';
}
const newPrefs = { ...preferences, ...changedPrefs };
browser.storage.local.set({ preferences: newPrefs });
},
set: (newPrefs) => () => newPrefs,
},
};
export default actions;

View File

@ -0,0 +1,49 @@
import { h } from 'hyperapp';
const OptionField = ({
id,
name,
type,
value,
options,
onChange,
}) => {
const handleChange = (e) => {
e.preventDefault();
const { target } = e;
const newValue = type === 'checkbox' ? target.checked : target.value;
onChange({ type, name, value: newValue });
};
switch (type) {
case 'select':
return (
<select id={id} name={name} value={value} onchange={handleChange}>
{options.map(({ value: val, label }) => (
<option key={val} value={val}>{label}</option>
))}
</select>
);
case 'text':
return (
<input type="text" id={id} name={name} value={value} onchange={handleChange} />
);
case 'checkbox':
return (
<input type="checkbox" id={id} name={name} checked={value} onchange={handleChange} />
);
case 'radio':
return options.map(({ value: val, label }) => (
<label key={val}>
<input type="radio" name={name} value={val} checked={val === value} onchange={handleChange} />
{label}
</label>
));
default:
return null;
}
};
export default OptionField;

View File

@ -0,0 +1,35 @@
import { h } from 'hyperapp';
import OptionField from './OptionField';
import ResetButton from './ResetButton';
const OptionsContainer = ({ options, preferences, onOptionChange }) => (
<div class="options">
{options.map(({
name,
title,
type,
options: opts,
}) => (
<div key={name} class="row">
<div class="col">
<label for={`option-${name}`}>{title}</label>
</div>
<div class="col">
<OptionField id={`option-${name}`} type={type} name={name} value={preferences[name]} options={opts} onChange={onOptionChange} />
</div>
</div>
))}
<div class="row">
<div class="col" />
<div class="col">
<ResetButton />
</div>
</div>
</div>
);
export default OptionsContainer;

View File

@ -0,0 +1,15 @@
import { h } from 'hyperapp';
const ResetButton = () => {
const handleClick = (e) => {
e.preventDefault();
browser.storage.local.set({ currRates: {} });
};
return (
<button onclick={handleClick}>Reset exchange rates</button>
);
};
export default ResetButton;

View File

@ -1,70 +1,18 @@
import getOptionRowElem from './utils/getOptionRowElem';
import getButtonRowElem from './utils/getButtonRowElem';
import getChangedPrefs from './utils/getChangedPrefs';
import { app } from 'hyperapp';
const onChange = ({ target }) => {
const changedPrefs = getChangedPrefs(target);
if (Object.keys(changedPrefs).length) {
browser.storage.local.get('preferences')
.then((storage) => {
browser.storage.local.set({ preferences: Object.assign({}, storage.preferences, changedPrefs) });
});
}
};
const onReset = () => {
browser.storage.local.set({ currRates: {} });
};
const buildOptionsForm = (options, preferences) => {
const optionsElem = document.getElementById('options');
options.forEach((option) => {
const value = preferences[option.name];
const optionRowElem = getOptionRowElem(option, value, onChange);
optionsElem.appendChild(optionRowElem);
});
const buttonRowElem = getButtonRowElem('Reset exchange rates', onReset);
optionsElem.appendChild(buttonRowElem);
};
const refreshOptionsForm = (changedPrefs) => {
Object.keys(changedPrefs).forEach((prefName) => {
const optionElem = document.querySelector(`[name="${prefName}"]`);
if (optionElem) {
if (optionElem.type === 'radio') {
const radioElem = document.querySelector(`[name="${prefName}"][value="${changedPrefs[prefName]}"]`);
if (radioElem) radioElem.checked = true;
} else if (optionElem.type === 'checkbox') {
optionElem.checked = changedPrefs[prefName];
} else {
optionElem.value = changedPrefs[prefName];
}
}
});
};
import actions from './actions';
import App from './App';
Promise.all([browser.runtime.getBackgroundPage(), browser.storage.local.get('preferences')])
.then(([bgWindow, storage]) => {
buildOptionsForm(bgWindow.OPTIONS, storage.preferences);
const state = {
options: bgWindow.OPTIONS,
preferences: storage.preferences,
};
browser.storage.onChanged.addListener((changes) => {
if (changes.preferences) {
const oldPrefs = changes.preferences.oldValue;
const newPrefs = changes.preferences.newValue;
const changedPrefs = {};
const main = app(state, actions, App, document.getElementById('app'));
Object.keys(changes.preferences.newValue).forEach((prefKey) => {
if (oldPrefs[prefKey] !== newPrefs[prefKey]) changedPrefs[prefKey] = newPrefs[prefKey];
});
if (Object.keys(changedPrefs).length) refreshOptionsForm(changedPrefs);
}
browser.storage.onChanged.addListener(({ preferences }) => {
if (preferences) main.preferences.set(preferences.newValue);
});
});

View File

@ -42,12 +42,12 @@ body {
padding: 0 5px 5px;
}
#options .col:first-child {
.options .col:first-child {
width: 100%;
}
@media (min-width: 500px) {
#options .col:first-child {
.options .col:first-child {
width: 250px;
}
}

View File

@ -7,11 +7,8 @@
<link rel="stylesheet" type="text/css" href="options.css">
</head>
<body>
<div class="container">
<h1>SCs Currency Converter - Options</h1>
<div id="app"></div>
<div id="options"></div>
</div>
<script type="text/javascript" src="options.js"></script>
</body>
</html>

View File

@ -1,20 +0,0 @@
export default (text, onClick) => {
const rowElem = document.createElement('div');
rowElem.className = 'row';
const emplyColElem = document.createElement('div');
emplyColElem.className = 'col';
rowElem.appendChild(emplyColElem);
const buttonColElem = document.createElement('div');
buttonColElem.className = 'col';
const buttonElem = document.createElement('button');
buttonElem.textContent = text;
buttonElem.addEventListener('click', onClick);
buttonColElem.appendChild(buttonElem);
rowElem.appendChild(buttonColElem);
return rowElem;
};

View File

@ -1,35 +0,0 @@
export default (target) => {
const prefName = target.name;
const prefValue = target.type === 'checkbox' ? target.checked : target.value;
const changedPrefs = {};
changedPrefs[prefName] = prefValue;
// change the symbol on currency change
if (prefName === 'toCurr') {
changedPrefs.symbol = prefValue;
}
// if a space inserted before the symbol, remove it, and set symbSep to true
if (prefName === 'symbol' && (prefValue.charAt(0) === ' ' || prefValue.charAt(prefValue.length - 1) === ' ')) {
const symbSepElem = document.getElementById('option-symbSep');
if (!symbSepElem.checked) changedPrefs.symbSep = true;
target.value = prefValue.trim();
delete changedPrefs.symbol;
}
// don't const to set the same thousand and decimal separator
if (prefName === 'sepTho' && [',', '.'].indexOf(prefValue) !== -1) {
const sepDecElem = document.getElementById('option-sepDec');
if (sepDecElem.value === prefValue) {
changedPrefs.sepDec = prefValue === ',' ? '.' : ',';
}
} else if (prefName === 'sepDec') {
const sepThoElem = document.getElementById('option-sepTho');
if (sepThoElem.value === prefValue) {
changedPrefs.sepTho = prefValue === ',' ? '.' : ',';
}
}
return changedPrefs;
};

View File

@ -1,74 +0,0 @@
export default (option, value, onChange) => {
const rowElem = document.createElement('div');
rowElem.className = 'row';
const labelColElem = document.createElement('div');
labelColElem.className = 'col';
const labelElem = document.createElement('label');
labelElem.htmlFor = `option-${option.name}`;
labelElem.textContent = option.title;
labelColElem.appendChild(labelElem);
rowElem.appendChild(labelColElem);
const optionColElem = document.createElement('div');
optionColElem.className = 'col';
let optionElem;
if (option.type === 'string') {
optionElem = document.createElement('input');
optionElem.type = 'text';
optionElem.name = option.name;
optionElem.value = value || '';
optionElem.addEventListener('input', onChange);
} else if (option.type === 'bool') {
optionElem = document.createElement('input');
optionElem.type = 'checkbox';
optionElem.name = option.name;
if (value === true) optionElem.checked = true;
optionElem.addEventListener('change', onChange);
} else if (option.type === 'radio') {
optionElem = document.createElement('div');
option.options.forEach((opt) => {
const optLabelElem = document.createElement('label');
const radioElem = document.createElement('input');
radioElem.type = 'radio';
radioElem.name = option.name;
radioElem.value = opt.value;
if (value === opt.value) radioElem.setAttribute('checked', '');
radioElem.addEventListener('change', onChange);
optLabelElem.appendChild(radioElem);
optLabelElem.appendChild(document.createTextNode(opt.label));
optionElem.appendChild(optLabelElem);
});
} else if (option.type === 'menulist') {
optionElem = document.createElement('select');
optionElem.name = option.name;
option.options.forEach((opt) => {
const optElem = document.createElement('option');
optElem.value = opt.value;
optElem.textContent = opt.label;
if (value === opt.value) optElem.setAttribute('selected', '');
optionElem.appendChild(optElem);
});
optionElem.addEventListener('change', onChange);
}
if (optionElem) {
optionElem.id = `option-${option.name}`;
optionColElem.appendChild(optionElem);
rowElem.appendChild(optionColElem);
}
return rowElem;
};