react-style-editor
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -53,3 +53,3 @@ "use strict"; | ||
// to combat `isInvalid` from upstream | ||
pointerEvents: 'auto' // to combat the general lock imposed by StyleEditor | ||
pointerEvents: 'auto !important' // to combat the general lock imposed by StyleEditor | ||
@@ -56,0 +56,0 @@ } |
@@ -145,9 +145,7 @@ "use strict"; | ||
content = _this$props2.content, | ||
onTick = _this$props2.onTick, | ||
onEditChange = _this$props2.onEditChange, | ||
onEditEnd = _this$props2.onEditEnd; | ||
onTick = _this$props2.onTick; | ||
var _this$state = this.state, | ||
isEditingContent = _this$state.isEditingContent, | ||
isEditingAfter = _this$state.isEditingAfter; | ||
var isLegit = !!content.match(/^\s*[-a-zA-Z0-9_]*\s*:|[{}()*@;\/\]]/); | ||
var isLegit = !!content.match(/^\s*[-a-zA-Z0-9_]*\s*:|[{}()*@;/\]]/); | ||
return _react.default.createElement("div", { | ||
@@ -154,0 +152,0 @@ className: classes.root, |
@@ -75,2 +75,4 @@ "use strict"; | ||
// Chrome | ||
textAlign: 'left', | ||
overflow: 'auto', | ||
color: 'black', | ||
@@ -94,6 +96,9 @@ position: 'relative', | ||
}, | ||
isEditing: { | ||
pointerEvents: 'none' | ||
isLocked: { | ||
'& *': { | ||
pointerEvents: 'none' | ||
} | ||
} | ||
}); // ===================================================================================================================== | ||
}); | ||
var hasControlledWarning = false; // ===================================================================================================================== | ||
// C O M P O N E N T | ||
@@ -121,7 +126,2 @@ // ===================================================================================================================== | ||
_defineProperty(_assertThisInitialized(_this), "state", { | ||
isEditing: false, | ||
hasArea: false | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "currentRules", []); | ||
@@ -133,3 +133,3 @@ | ||
_defineProperty(_assertThisInitialized(_this), "previousPropsCSS", void 0); | ||
_defineProperty(_assertThisInitialized(_this), "isControlled", false); | ||
@@ -149,115 +149,109 @@ _defineProperty(_assertThisInitialized(_this), "computeRules", function (css) { | ||
_defineProperty(_assertThisInitialized(_this), "computeRulesFromPayload", function (id, payload) { | ||
var _modify = (0, _modify2.default)(_this.currentRules, id, payload), | ||
freshRules = _modify.freshRules, | ||
freshNode = _modify.freshNode, | ||
parentNode = _modify.parentNode; | ||
_defineProperty(_assertThisInitialized(_this), "onEditBegin", function () { | ||
_this.setState({ | ||
isEditing: true | ||
}); | ||
}); | ||
if (payload[_COMMON.AFTER_BEGIN]) { | ||
// can only be dispatched by AT/RULE | ||
var node = createTemporaryDeclaration(payload[_COMMON.AFTER_BEGIN]); | ||
freshNode.kids.unshift(node); | ||
} else if (payload[_COMMON.BEFORE]) { | ||
// can only be dispatched by AT/RULE and can only create AT/RULE | ||
var _node = createTemporaryRule(payload[_COMMON.BEFORE]); | ||
_defineProperty(_assertThisInitialized(_this), "onEditChange", function (id, payload) { | ||
var onChange = _this.props.onChange; | ||
var siblings = parentNode.kids; | ||
var index = siblings.findIndex(function (item) { | ||
return item.id === id; | ||
}); | ||
siblings.splice(index, 0, _node); | ||
} else if (payload[_COMMON.AFTER]) { | ||
// can be dispatched by any type of node | ||
var text = payload[_COMMON.AFTER]; | ||
if (onChange) { | ||
var freshBlob = computeBlobFromPayload(_this.currentRules, id, payload); | ||
var _node2; | ||
_this.announceOnChange(freshBlob); | ||
} | ||
}); | ||
switch (freshNode.type) { | ||
// freshNode is in fact the anchor node, NOT the node we're about to create | ||
case _COMMON.ATRULE: | ||
if (freshNode.hasBraceBegin && !freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} else if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
_defineProperty(_assertThisInitialized(_this), "announceOnChange", function (rulesOrBlob) { | ||
var _this$props = _this.props, | ||
onChange = _this$props.onChange, | ||
outputFormats = _this$props.outputFormats; | ||
_node2 = createTemporaryRule(text); | ||
break; | ||
if (onChange) { | ||
var rules = typeof rulesOrBlob === 'string' ? null : rulesOrBlob; // null means lazy initialization | ||
case _COMMON.RULE: | ||
if (!freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} | ||
var formats = outputFormats.replace(/\s/g, '').split(','); | ||
var output = []; | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
_node2 = createTemporaryRule(text); | ||
break; | ||
try { | ||
for (var _iterator = formats[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var format = _step.value; | ||
case _COMMON.DECLARATION: | ||
if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
switch (format) { | ||
case 'preserved': | ||
if (rules) { | ||
output.push((0, _stringify.default)(rulesOrBlob)); | ||
} else { | ||
output.push(rulesOrBlob); | ||
} | ||
_node2 = createTemporaryDeclaration(text); | ||
break; | ||
break; | ||
case _COMMON.COMMENT: | ||
if (!freshNode.hasSlashEnd) { | ||
text = '*/' + text; | ||
} | ||
case 'machine': | ||
if (!rules) { | ||
rules = _this.computeRules(rulesOrBlob); | ||
} | ||
if (parentNode.type === _COMMON.ATRULE) { | ||
_node2 = createTemporaryRule(text); | ||
} else { | ||
_node2 = createTemporaryDeclaration(text); | ||
} | ||
output.push(JSON.parse(JSON.stringify(rules))); // TODO: use something faster | ||
break; | ||
} | ||
break; | ||
var _siblings = parentNode.kids; | ||
case 'pretty': | ||
default: | ||
if (!rules) { | ||
rules = _this.computeRules(rulesOrBlob); | ||
} | ||
var _index = _siblings.findIndex(function (item) { | ||
return item.id === id; | ||
}); | ||
output.push((0, _prettify.default)(rules)); | ||
break; | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return != null) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
_siblings.splice(_index + 1, 0, _node2); | ||
} else if (payload.value) { | ||
freshNode.hasColon = true; | ||
onChange(output.length > 1 ? output : output[0] || ''); | ||
} | ||
var temporaryBlob = (0, _stringify.default)(freshRules); // console.log(temporaryBlob); | ||
return _this.computeRules(temporaryBlob); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onEditBegin", function () { | ||
_this.setState({ | ||
isEditing: true | ||
}); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onEditEnd", function (id, payload) { | ||
if (_this.isControlled) { | ||
_this.setState({ | ||
isEditing: false | ||
}); // there's no need to do anything else. Our parent already has the payload from the onChange event | ||
_defineProperty(_assertThisInitialized(_this), "onEditChange", function (id, payload) { | ||
var onChange = _this.props.onChange; | ||
if (onChange) { | ||
var freshRules = _this.computeRulesFromPayload(id, payload); | ||
var prettyBlob = (0, _prettify.default)(freshRules); | ||
onChange(prettyBlob); | ||
} else { | ||
// uncontrolled | ||
_this.setState({ | ||
isEditing: false, | ||
internalValue: computeBlobFromPayload(_this.currentRules, id, payload) | ||
}); | ||
} | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onEditEnd", function (id, payload) { | ||
_this.currentRules = _this.computeRulesFromPayload(id, payload); | ||
_this.setState({ | ||
isEditing: false | ||
}); | ||
}); | ||
_defineProperty(_assertThisInitialized(_this), "onTick", function (id, desiredTick) { | ||
var freshBlob = desiredTick ? (0, _unignore.default)(_this.currentRules, id) : (0, _ignore.default)(_this.currentRules, id); | ||
_this.currentRules = _this.computeRules(freshBlob); | ||
_this.forceUpdate(); | ||
if (_this.isControlled) { | ||
_this.announceOnChange(freshBlob); | ||
} else { | ||
_this.setState({ | ||
internalValue: freshBlob | ||
}); | ||
} | ||
}); | ||
@@ -283,4 +277,3 @@ | ||
if (onChange) { | ||
var prettyBlob = (0, _prettify.default)(_this.computeRules(payload.selector)); | ||
onChange(prettyBlob); | ||
_this.announceOnChange(payload.selector); | ||
} | ||
@@ -290,11 +283,24 @@ }); | ||
_defineProperty(_assertThisInitialized(_this), "onAreaBlur", function (id, payload) { | ||
_this.currentRules = _this.computeRules(payload.selector); | ||
if (_this.isControlled) { | ||
_this.setState({ | ||
isEditing: false, | ||
hasArea: false | ||
}); // there's no need to do anything else. Our parent already has the payload from the onChange event | ||
_this.setState({ | ||
isEditing: false, | ||
hasArea: false | ||
}); | ||
} else { | ||
// uncontrolled | ||
_this.setState({ | ||
isEditing: false, | ||
hasArea: false, | ||
internalValue: payload.selector | ||
}); | ||
} | ||
}); | ||
(0, _stylize.prepareStyling)(); | ||
_this.state = { | ||
isEditing: false, | ||
hasArea: false, | ||
internalValue: props.defaultValue | ||
}; | ||
return _this; | ||
@@ -310,18 +316,17 @@ } | ||
value: function render() { | ||
var _this$props = this.props, | ||
css = _this$props.css, | ||
other = _objectWithoutProperties(_this$props, ["css"]); | ||
var _this$props2 = this.props, | ||
value = _this$props2.value, | ||
className = _this$props2.className, | ||
readOnly = _this$props2.readOnly, | ||
other = _objectWithoutProperties(_this$props2, ["value", "className", "readOnly"]); | ||
var _this$state = this.state, | ||
isEditing = _this$state.isEditing, | ||
hasArea = _this$state.hasArea; | ||
hasArea = _this$state.hasArea, | ||
internalValue = _this$state.internalValue; | ||
delete other.outputFormats; // not used in render | ||
if (css !== this.previousPropsCSS) { | ||
// our parent changed the css! | ||
this.currentRules = this.computeRules(css); | ||
this.previousPropsCSS = css; | ||
} else {// the local logic already computed the rules | ||
// nothing to do | ||
} | ||
this.isControlled = checkIsControlled(this.props); | ||
var usedValue = this.isControlled ? value : internalValue; | ||
this.currentRules = this.computeRules(usedValue); | ||
var isEmpty = !this.currentRules.length; | ||
@@ -332,3 +337,3 @@ return _react.default.createElement("div", _extends({ | ||
}, other, { | ||
className: (0, _cls.default)(classes.root, isEmpty && !hasArea && classes.isEmpty, isEditing && classes.isEditing) | ||
className: (0, _cls.default)(classes.root, isEmpty && !hasArea && classes.isEmpty, (isEditing || readOnly) && classes.isLocked, className) | ||
}), !isEmpty && _react.default.createElement(_Rule.default, { | ||
@@ -351,2 +356,11 @@ selector: 'root', | ||
/** | ||
* | ||
*/ | ||
}, { | ||
key: "componentDidMount", | ||
value: function componentDidMount() { | ||
this.announceOnChange(this.currentRules); | ||
} | ||
/** | ||
* Under no circumstances do we allow updates while an edit is on-going. | ||
@@ -366,3 +380,6 @@ * Alas, because of this small restriction, we had to quit using PureComponent and had to duplicate its | ||
if (this.props[key] !== nextProps[key]) { | ||
return true; | ||
if (key !== 'defaultValue') { | ||
// we're ignoring changes to defaultValue | ||
return true; | ||
} | ||
} | ||
@@ -404,2 +421,110 @@ } | ||
var checkIsControlled = function checkIsControlled(props) { | ||
if (props.value !== undefined) { | ||
if (!props.onChange && !props.readOnly && !hasControlledWarning) { | ||
hasControlledWarning = true; | ||
if (window.console && window.console.warn) { | ||
console.warn('You provided a `value` prop to StyleEditor without an `onChange` handler. ' + 'This will render a read-only field. If the StyleEditor should be mutable, use `defaultValue`. ' + 'Otherwise, set either `onChange` or `readOnly`.'); | ||
} | ||
} | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
}; | ||
/** | ||
* | ||
*/ | ||
var computeBlobFromPayload = function computeBlobFromPayload(rules, id, payload) { | ||
var _modify = (0, _modify2.default)(rules, id, payload), | ||
freshRules = _modify.freshRules, | ||
freshNode = _modify.freshNode, | ||
parentNode = _modify.parentNode; | ||
if (payload[_COMMON.AFTER_BEGIN]) { | ||
// can only be dispatched by AT/RULE | ||
var node = createTemporaryDeclaration(payload[_COMMON.AFTER_BEGIN]); | ||
freshNode.kids.unshift(node); | ||
} else if (payload[_COMMON.BEFORE]) { | ||
// can only be dispatched by AT/RULE and can only create AT/RULE | ||
var _node = createTemporaryRule(payload[_COMMON.BEFORE]); | ||
var siblings = parentNode.kids; | ||
var index = siblings.findIndex(function (item) { | ||
return item.id === id; | ||
}); | ||
siblings.splice(index, 0, _node); | ||
} else if (payload[_COMMON.AFTER]) { | ||
// can be dispatched by any type of node | ||
var text = payload[_COMMON.AFTER]; | ||
var _node2; | ||
switch (freshNode.type) { | ||
// freshNode is in fact the anchor node, NOT the node we're about to create | ||
case _COMMON.ATRULE: | ||
if (freshNode.hasBraceBegin && !freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} else if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
_node2 = createTemporaryRule(text); | ||
break; | ||
case _COMMON.RULE: | ||
if (!freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} | ||
_node2 = createTemporaryRule(text); | ||
break; | ||
case _COMMON.DECLARATION: | ||
if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
_node2 = createTemporaryDeclaration(text); | ||
break; | ||
case _COMMON.COMMENT: | ||
if (!freshNode.hasSlashEnd) { | ||
text = '*/' + text; | ||
} | ||
if (parentNode.type === _COMMON.ATRULE) { | ||
_node2 = createTemporaryRule(text); | ||
} else { | ||
_node2 = createTemporaryDeclaration(text); | ||
} | ||
break; | ||
default: // nothing | ||
} | ||
var _siblings = parentNode.kids; | ||
var _index = _siblings.findIndex(function (item) { | ||
return item.id === id; | ||
}); | ||
_siblings.splice(_index + 1, 0, _node2); | ||
} else if (payload.value) { | ||
freshNode.hasColon = true; | ||
} | ||
return (0, _stringify.default)(freshRules); | ||
}; | ||
/** | ||
* | ||
*/ | ||
var createTemporaryDeclaration = function createTemporaryDeclaration(text) { | ||
@@ -446,3 +571,10 @@ if (!text.match(/;\s*$/)) { | ||
StyleEditor.defaultProps = { | ||
outputFormats: 'pretty', | ||
onChange: null, | ||
defaultValue: '', | ||
value: undefined, | ||
readOnly: false | ||
}; | ||
var _default = StyleEditor; | ||
exports.default = _default; |
@@ -41,2 +41,5 @@ "use strict"; | ||
break; | ||
default: // nothing | ||
} | ||
@@ -43,0 +46,0 @@ |
@@ -85,2 +85,5 @@ "use strict"; | ||
break; | ||
default: // nothing | ||
} | ||
@@ -87,0 +90,0 @@ } |
@@ -79,2 +79,5 @@ "use strict"; | ||
break; | ||
default: // nothing | ||
} | ||
@@ -81,0 +84,0 @@ } |
@@ -54,2 +54,5 @@ "use strict"; | ||
break; | ||
default: // nothing | ||
} | ||
@@ -56,0 +59,0 @@ } |
@@ -18,6 +18,6 @@ "use strict"; | ||
var isAppended = false; | ||
var registry = {}; | ||
var cssCollection = []; | ||
var style = document.createElement('style'); | ||
var count = 0; | ||
/** | ||
@@ -87,6 +87,8 @@ * | ||
var prepareStyling = function prepareStyling() { | ||
if (!isAppended) { | ||
count++; | ||
if (count === 1) { | ||
// TODO: study impact on hot loading | ||
style.innerHTML = cssCollection.join(''); | ||
document.head.appendChild(style); | ||
isAppended = true; | ||
} | ||
@@ -102,6 +104,7 @@ }; | ||
var releaseStyling = function releaseStyling() { | ||
if (isAppended) { | ||
count--; | ||
if (count === 0) { | ||
document.head.removeChild(style); | ||
style.innerHTML = ''; | ||
isAppended = false; | ||
} | ||
@@ -108,0 +111,0 @@ }; |
{ | ||
"name": "react-style-editor", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "A React component that displays and edits CSS, similar to the browser's DevTools.", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
# React Style Editor | ||
[![Npm Version][npm-version-image]][npm-version-url] [![License][license-image]][license-url] | ||
[![Size][bundlephobia-image]][bundlephobia-url] | ||
[![Npm Version][npm-version-image]][npm-version-url] [![Size][bundlephobia-image]][bundlephobia-url] | ||
@@ -10,2 +9,6 @@ | ||
[![Live demo](https://aurelain.github.io/react-style-editor/StyleEditor.png)](https://aurelain.github.io/react-style-editor/) | ||
## [Live demo](https://aurelain.github.io/react-style-editor/) | ||
## Features | ||
@@ -17,7 +20,8 @@ - Parses any CSS string and formats it in a familiar fashion | ||
- Has no dependencies (other than React) | ||
- Is tiny (< 10 KB minified) | ||
- Is customizable through classes | ||
- Offers 3 output formats: | ||
- the code with preserved formatting | ||
- a machine-friendly model of the code (recursive array of objects) | ||
- the prettified code | ||
- a machine-friendly model of the code (recursive array of objects) | ||
@@ -41,3 +45,3 @@ ## Installation | ||
<StyleEditor | ||
css={` | ||
defaultValue={` | ||
div {color:red;} | ||
@@ -57,8 +61,30 @@ /* Hello, World! */ | ||
## Props | ||
|prop | type | default |description | | ||
|---------------|------------------------|----------------------------------------------------|--------| | ||
| `defaultValue` | string | `''` | The initial CSS code | ||
| `value` | string | `undefined` | The controlled CSS code | ||
| `onChange` | function | `null` | A closure that receives a single argument, `string` or `array`, depending on the value of `outputFormats` | ||
| `outputFormats` | string | `'pretty'` | Comma-separated values of: `'preserved'`, `'machine'`, `'pretty'` | ||
| `readOnly` | boolean | `false` | All interactions with the component are blocked | ||
All parameters are optional, but some are inter-related. For example, due to the nature of React, you should use `StyleEditor` either fully controlled or fully uncontrolled (see [this article](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#preferred-solutions)). | ||
A short summary: | ||
- `defaultValue` => uncontrolled, the component is on its own | ||
- `value` => controlled => you must also use the `onChange` or `readOnly` properties. | ||
The above behavior is identical to that of normal React form elements, e.g. `<textarea/>`. | ||
Any other props are spread to the internal root. | ||
## Ideas for the future | ||
- Live demo | ||
- Color swatches (similar to the browser) | ||
- Dropdown suggestions for properties/values (similar to the browser) | ||
- Ability to copy/delete fragments of code | ||
- Keyboard support for `TAB`, `:` and `UP`, `DOWN` | ||
- Keyboard support for `TAB`, `:` and `UP`/`DOWN` increments of numeric values | ||
- Prop for automatically mutating the code *after* validation | ||
@@ -76,6 +102,3 @@ - Theme support (similar to the browser) | ||
[license-image]: http://img.shields.io/npm/l/react-style-editor.svg?style=flat-square | ||
[license-url]: https://github.com/Aurelain/react-style-editor/blob/master/LICENSE | ||
[bundlephobia-image]: https://img.shields.io/bundlephobia/minzip/react-style-editor.svg?style=flat-square | ||
[bundlephobia-url]: https://bundlephobia.com/result?p=react-style-editor |
@@ -22,3 +22,3 @@ import React from 'react'; | ||
textDecoration: 'none', // to combat `isInvalid` from upstream | ||
pointerEvents: 'auto', // to combat the general lock imposed by StyleEditor | ||
pointerEvents: 'auto !important', // to combat the general lock imposed by StyleEditor | ||
}, | ||
@@ -25,0 +25,0 @@ }); |
@@ -47,6 +47,6 @@ import React from 'react'; | ||
render() { | ||
const {id, content, onTick, onEditChange, onEditEnd} = this.props; | ||
const {id, content, onTick} = this.props; | ||
const {isEditingContent, isEditingAfter} = this.state; | ||
const isLegit = !!content.match(/^\s*[-a-zA-Z0-9_]*\s*:|[{}()*@;\/\]]/); | ||
const isLegit = !!content.match(/^\s*[-a-zA-Z0-9_]*\s*:|[{}()*@;/\]]/); | ||
@@ -53,0 +53,0 @@ return ( |
@@ -24,2 +24,4 @@ import React from 'react'; | ||
fontSize: '12px', // Chrome | ||
textAlign: 'left', | ||
overflow: 'auto', | ||
color: 'black', | ||
@@ -43,6 +45,9 @@ position: 'relative', | ||
}, | ||
isEditing: { | ||
pointerEvents: 'none', | ||
isLocked: { | ||
'& *': { | ||
pointerEvents: 'none', | ||
} | ||
}, | ||
}); | ||
let hasControlledWarning = false; | ||
@@ -54,7 +59,2 @@ // ===================================================================================================================== | ||
state = { | ||
isEditing: false, | ||
hasArea: false, | ||
}; | ||
// Private variables: | ||
@@ -64,3 +64,3 @@ currentRules = []; | ||
memoCSS = ''; // a simulation of `memoize-one` | ||
previousPropsCSS; | ||
isControlled = false; | ||
@@ -73,2 +73,7 @@ /** | ||
prepareStyling(); | ||
this.state = { | ||
isEditing: false, | ||
hasArea: false, | ||
internalValue: props.defaultValue, | ||
}; | ||
} | ||
@@ -80,11 +85,10 @@ | ||
render() { | ||
const {css, ...other} = this.props; | ||
const {isEditing, hasArea} = this.state; | ||
const {value, className, readOnly, ...other} = this.props; | ||
const {isEditing, hasArea, internalValue} = this.state; | ||
delete other.outputFormats; // not used in render | ||
if (css !== this.previousPropsCSS) { // our parent changed the css! | ||
this.currentRules = this.computeRules(css); | ||
this.previousPropsCSS = css; | ||
} else { // the local logic already computed the rules | ||
// nothing to do | ||
} | ||
this.isControlled = checkIsControlled(this.props); | ||
const usedValue = this.isControlled ? value : internalValue; | ||
this.currentRules = this.computeRules(usedValue); | ||
const isEmpty = !this.currentRules.length; | ||
@@ -97,3 +101,8 @@ | ||
{...other} | ||
className={cls(classes.root, isEmpty && !hasArea && classes.isEmpty, isEditing && classes.isEditing)} | ||
className={cls( | ||
classes.root, | ||
isEmpty && !hasArea && classes.isEmpty, | ||
(isEditing || readOnly) && classes.isLocked, | ||
className, | ||
)} | ||
> | ||
@@ -127,2 +136,9 @@ { | ||
/** | ||
* | ||
*/ | ||
componentDidMount() { | ||
this.announceOnChange(this.currentRules); | ||
} | ||
/** | ||
* Under no circumstances do we allow updates while an edit is on-going. | ||
@@ -138,3 +154,5 @@ * Alas, because of this small restriction, we had to quit using PureComponent and had to duplicate its | ||
if (this.props[key] !== nextProps[key]) { | ||
return true; | ||
if (key !== 'defaultValue') { // we're ignoring changes to defaultValue | ||
return true; | ||
} | ||
} | ||
@@ -175,64 +193,2 @@ } | ||
*/ | ||
computeRulesFromPayload = (id, payload) => { | ||
const {freshRules, freshNode, parentNode} = modify(this.currentRules, id, payload); | ||
if (payload[AFTER_BEGIN]) { // can only be dispatched by AT/RULE | ||
const node = createTemporaryDeclaration(payload[AFTER_BEGIN]); | ||
freshNode.kids.unshift(node); | ||
} else if (payload[BEFORE]) { // can only be dispatched by AT/RULE and can only create AT/RULE | ||
const node = createTemporaryRule(payload[BEFORE]); | ||
const siblings = parentNode.kids; | ||
const index = siblings.findIndex(item => item.id === id); | ||
siblings.splice(index, 0, node); | ||
} else if (payload[AFTER]) { // can be dispatched by any type of node | ||
let text = payload[AFTER]; | ||
let node; | ||
switch (freshNode.type) { // freshNode is in fact the anchor node, NOT the node we're about to create | ||
case ATRULE: | ||
if (freshNode.hasBraceBegin && !freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} else if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
node = createTemporaryRule(text); | ||
break; | ||
case RULE: | ||
if (!freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} | ||
node = createTemporaryRule(text); | ||
break; | ||
case DECLARATION: | ||
if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
node = createTemporaryDeclaration(text); | ||
break; | ||
case COMMENT: | ||
if (!freshNode.hasSlashEnd) { | ||
text = '*/' + text; | ||
} | ||
if (parentNode.type === ATRULE) { | ||
node = createTemporaryRule(text); | ||
} else { | ||
node = createTemporaryDeclaration(text); | ||
} | ||
break; | ||
} | ||
const siblings = parentNode.kids; | ||
const index = siblings.findIndex(item => item.id === id); | ||
siblings.splice(index + 1, 0, node); | ||
} else if (payload.value) { | ||
freshNode.hasColon = true; | ||
} | ||
const temporaryBlob = stringify(freshRules); | ||
// console.log(temporaryBlob); | ||
return this.computeRules(temporaryBlob); | ||
}; | ||
/** | ||
* | ||
*/ | ||
onEditBegin = () => { | ||
@@ -250,5 +206,4 @@ this.setState({ | ||
if (onChange) { | ||
const freshRules = this.computeRulesFromPayload(id, payload); | ||
const prettyBlob = prettify(freshRules); | ||
onChange(prettyBlob); | ||
const freshBlob = computeBlobFromPayload(this.currentRules, id, payload); | ||
this.announceOnChange(freshBlob); | ||
} | ||
@@ -260,7 +215,52 @@ }; | ||
*/ | ||
announceOnChange = (rulesOrBlob) => { | ||
const {onChange, outputFormats} = this.props; | ||
if (onChange) { | ||
let rules = typeof rulesOrBlob === 'string'? null : rulesOrBlob; // null means lazy initialization | ||
const formats = outputFormats.replace(/\s/g, '').split(','); | ||
const output = []; | ||
for (const format of formats) { | ||
switch (format) { | ||
case 'preserved': | ||
if (rules) { | ||
output.push(stringify(rulesOrBlob)); | ||
} else { | ||
output.push(rulesOrBlob); | ||
} | ||
break; | ||
case 'machine': | ||
if (!rules) { | ||
rules = this.computeRules(rulesOrBlob); | ||
} | ||
output.push(JSON.parse(JSON.stringify(rules))); // TODO: use something faster | ||
break; | ||
case 'pretty': | ||
default: | ||
if (!rules) { | ||
rules = this.computeRules(rulesOrBlob); | ||
} | ||
output.push(prettify(rules)); | ||
break; | ||
} | ||
} | ||
onChange(output.length > 1 ? output : (output[0] || '')); | ||
} | ||
}; | ||
/** | ||
* | ||
*/ | ||
onEditEnd = (id, payload) => { | ||
this.currentRules = this.computeRulesFromPayload(id, payload); | ||
this.setState({ | ||
isEditing: false, | ||
}); | ||
if (this.isControlled) { | ||
this.setState({ | ||
isEditing: false, | ||
}); | ||
// there's no need to do anything else. Our parent already has the payload from the onChange event | ||
} else { // uncontrolled | ||
this.setState({ | ||
isEditing: false, | ||
internalValue: computeBlobFromPayload(this.currentRules, id, payload) | ||
}); | ||
} | ||
}; | ||
@@ -273,4 +273,9 @@ | ||
const freshBlob = desiredTick ? unignore(this.currentRules, id) : ignore(this.currentRules, id); | ||
this.currentRules = this.computeRules(freshBlob); | ||
this.forceUpdate(); | ||
if (this.isControlled) { | ||
this.announceOnChange(freshBlob); | ||
} else { | ||
this.setState({ | ||
internalValue: freshBlob, | ||
}) | ||
} | ||
}; | ||
@@ -304,4 +309,3 @@ | ||
if (onChange) { | ||
const prettyBlob = prettify(this.computeRules(payload.selector)); | ||
onChange(prettyBlob); | ||
this.announceOnChange(payload.selector); | ||
} | ||
@@ -314,7 +318,16 @@ }; | ||
onAreaBlur = (id, payload) => { | ||
this.currentRules = this.computeRules(payload.selector); | ||
this.setState({ | ||
isEditing: false, | ||
hasArea: false, | ||
}); | ||
if (this.isControlled) { | ||
this.setState({ | ||
isEditing: false, | ||
hasArea: false, | ||
}); | ||
// there's no need to do anything else. Our parent already has the payload from the onChange event | ||
} else { // uncontrolled | ||
this.setState({ | ||
isEditing: false, | ||
hasArea: false, | ||
internalValue: payload.selector | ||
}); | ||
} | ||
}; | ||
@@ -330,2 +343,83 @@ | ||
*/ | ||
const checkIsControlled = (props) => { | ||
if (props.value !== undefined) { | ||
if (!props.onChange && !props.readOnly && !hasControlledWarning) { | ||
hasControlledWarning = true; | ||
if (window.console && window.console.warn) { | ||
console.warn('You provided a `value` prop to StyleEditor without an `onChange` handler. ' + | ||
'This will render a read-only field. If the StyleEditor should be mutable, use `defaultValue`. ' + | ||
'Otherwise, set either `onChange` or `readOnly`.'); | ||
} | ||
} | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
}; | ||
/** | ||
* | ||
*/ | ||
const computeBlobFromPayload = (rules, id, payload) => { | ||
const {freshRules, freshNode, parentNode} = modify(rules, id, payload); | ||
if (payload[AFTER_BEGIN]) { // can only be dispatched by AT/RULE | ||
const node = createTemporaryDeclaration(payload[AFTER_BEGIN]); | ||
freshNode.kids.unshift(node); | ||
} else if (payload[BEFORE]) { // can only be dispatched by AT/RULE and can only create AT/RULE | ||
const node = createTemporaryRule(payload[BEFORE]); | ||
const siblings = parentNode.kids; | ||
const index = siblings.findIndex(item => item.id === id); | ||
siblings.splice(index, 0, node); | ||
} else if (payload[AFTER]) { // can be dispatched by any type of node | ||
let text = payload[AFTER]; | ||
let node; | ||
switch (freshNode.type) { // freshNode is in fact the anchor node, NOT the node we're about to create | ||
case ATRULE: | ||
if (freshNode.hasBraceBegin && !freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} else if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
node = createTemporaryRule(text); | ||
break; | ||
case RULE: | ||
if (!freshNode.hasBraceEnd) { | ||
text = '}' + text; | ||
} | ||
node = createTemporaryRule(text); | ||
break; | ||
case DECLARATION: | ||
if (!freshNode.hasSemicolon) { | ||
text = ';' + text; | ||
} | ||
node = createTemporaryDeclaration(text); | ||
break; | ||
case COMMENT: | ||
if (!freshNode.hasSlashEnd) { | ||
text = '*/' + text; | ||
} | ||
if (parentNode.type === ATRULE) { | ||
node = createTemporaryRule(text); | ||
} else { | ||
node = createTemporaryDeclaration(text); | ||
} | ||
break; | ||
default: | ||
// nothing | ||
} | ||
const siblings = parentNode.kids; | ||
const index = siblings.findIndex(item => item.id === id); | ||
siblings.splice(index + 1, 0, node); | ||
} else if (payload.value) { | ||
freshNode.hasColon = true; | ||
} | ||
return stringify(freshRules); | ||
}; | ||
/** | ||
* | ||
*/ | ||
const createTemporaryDeclaration = (text) => { | ||
@@ -364,2 +458,9 @@ if (!text.match(/;\s*$/)) { // doesn't end with semicolon | ||
// ===================================================================================================================== | ||
StyleEditor.defaultProps = { | ||
outputFormats: 'pretty', | ||
onChange: null, | ||
defaultValue: '', | ||
value: undefined, | ||
readOnly: false, | ||
}; | ||
export default StyleEditor; |
@@ -30,2 +30,4 @@ /* | ||
break; | ||
default: | ||
// nothing | ||
} | ||
@@ -32,0 +34,0 @@ if (id in usedIds) { |
@@ -69,2 +69,4 @@ /* | ||
break; | ||
default: | ||
// nothing | ||
} | ||
@@ -71,0 +73,0 @@ } |
@@ -47,2 +47,4 @@ /* | ||
break; | ||
default: | ||
// nothing | ||
} | ||
@@ -49,0 +51,0 @@ } |
@@ -49,2 +49,4 @@ /* | ||
break; | ||
default: | ||
// nothing | ||
} | ||
@@ -51,0 +53,0 @@ } |
@@ -9,6 +9,6 @@ /* | ||
let isAppended = false; | ||
let registry = {}; | ||
let cssCollection = []; | ||
let style = document.createElement('style'); | ||
let count = 0; | ||
@@ -67,6 +67,6 @@ | ||
const prepareStyling = () => { | ||
if (!isAppended) { | ||
count++; | ||
if (count === 1) { // TODO: study impact on hot loading | ||
style.innerHTML = cssCollection.join(''); | ||
document.head.appendChild(style); | ||
isAppended = true; | ||
} | ||
@@ -79,6 +79,6 @@ }; | ||
const releaseStyling = () => { | ||
if (isAppended) { | ||
count--; | ||
if (count === 0) { | ||
document.head.removeChild(style); | ||
style.innerHTML = ''; | ||
isAppended = false; | ||
} | ||
@@ -85,0 +85,0 @@ }; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
179752
4714
99