",
11 | "license": "MIT",
12 | "homepage": "https://github.com/Aurelain/react-style-editor/",
13 | "keywords": [
14 | "react",
15 | "style",
16 | "editor",
17 | "CSS",
18 | "rules",
19 | "declarations",
20 | "comment",
21 | "checkbox",
22 | "validator"
23 | ],
24 | "scripts": {
25 | "storybook": "storybook dev -c .storybook",
26 | "lib": "rm -rf lib && mkdir lib && babel src -d lib",
27 | "collect-database": "node scripts/collect-database.js"
28 | },
29 | "devDependencies": {
30 | "@babel/cli": "^7.4.4",
31 | "@babel/core": "^7.4.4",
32 | "@babel/plugin-proposal-class-properties": "^7.4.4",
33 | "@babel/preset-env": "^7.4.4",
34 | "@babel/preset-react": "^7.0.0",
35 | "@storybook/react": "^7.0.22",
36 | "@storybook/react-webpack5": "^7.0.22",
37 | "babel-loader": "^8.0.5",
38 | "prettier": "^2.4.1",
39 | "react": "^16.8.6",
40 | "react-dom": "^16.8.6",
41 | "storybook": "^7.0.22"
42 | },
43 | "files": [
44 | "lib",
45 | "src"
46 | ],
47 | "babel": {
48 | "presets": [
49 | "@babel/preset-env",
50 | "@babel/preset-react"
51 | ],
52 | "plugins": [
53 | "@babel/plugin-proposal-class-properties"
54 | ]
55 | },
56 | "prettier": {
57 | "tabWidth": 4,
58 | "singleQuote": true,
59 | "printWidth": 120,
60 | "bracketSpacing": false
61 | },
62 | "bugs": {
63 | "url": "https://github.com/Aurelain/react-style-editor/issues"
64 | },
65 | "directories": {
66 | "lib": "lib",
67 | "test": "tests"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/utils/stylize.js:
--------------------------------------------------------------------------------
1 | /*
2 | A quick-and-dirty simulation of JSS.
3 | */
4 |
5 | const PREFIX = 'rse';
6 | const SEPARATOR = '-';
7 | const dashConverter = (match) => '-' + match.toLowerCase();
8 |
9 | let registry = {};
10 | let cssCollection = [];
11 | let style = document.createElement('style');
12 | let count = 0;
13 |
14 | /**
15 | *
16 | */
17 | const stylize = (name, classes) => {
18 | const output = {};
19 | const css = collect(name, classes, output);
20 | const index = registry[name];
21 | if (index === undefined) {
22 | registry[name] = cssCollection.push(css) - 1;
23 | } else {
24 | cssCollection[index] = css;
25 | }
26 | return output;
27 | };
28 |
29 | /**
30 | *
31 | */
32 | const collect = (name, classes, accumulator = {}) => {
33 | let css = '';
34 | for (const selector in classes) {
35 | const block = classes[selector];
36 | const className = PREFIX + SEPARATOR + name + SEPARATOR + selector;
37 | css += '.' + className + '{\r\n';
38 | const nested = {};
39 | for (const property in block) {
40 | const value = block[property];
41 | if (property.indexOf('&') >= 0) {
42 | // this is in fact a nested selector, not a declaration
43 | const resolved = property.replace(/&/g, selector);
44 | nested[resolved] = value;
45 | continue;
46 | }
47 | const cssProperty = property.replace(/([A-Z])/g, dashConverter);
48 | const cssValue = value + (typeof value === 'number' ? 'px' : '');
49 | css += ' ' + cssProperty + ':' + cssValue + ';\r\n';
50 | }
51 | css += '}\r\n';
52 | if (Object.keys(nested).length) {
53 | css += collect(name, nested);
54 | }
55 | accumulator[selector] = className;
56 | }
57 | return css;
58 | };
59 |
60 | /**
61 | *
62 | */
63 | const prepareStyling = () => {
64 | count++;
65 | if (count === 1) {
66 | // TODO: study impact on hot loading
67 | style.innerHTML = cssCollection.join('');
68 | document.head.appendChild(style);
69 | }
70 | };
71 |
72 | /**
73 | *
74 | */
75 | const releaseStyling = () => {
76 | count--;
77 | if (count === 0) {
78 | document.head.removeChild(style);
79 | style.innerHTML = '';
80 | }
81 | };
82 |
83 | export default stylize;
84 | export {prepareStyling, releaseStyling};
85 |
--------------------------------------------------------------------------------
/src/utils/ignore.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | */
4 |
5 | import modify from './modify.js';
6 | import stringify from './stringify.js';
7 | import {ATRULE, DECLARATION, RULE, COMMENT, SLASH_SUBSTITUTE} from './COMMON.js';
8 |
9 | /**
10 | *
11 | */
12 | const ignore = (oldRules, id) => {
13 | const {freshRules, freshNode} = modify(oldRules, id, {}); // blank change to get the `freshNode`
14 | const content = stringifyAndHandleComments([freshNode]);
15 | for (const key in freshNode) {
16 | delete freshNode[key];
17 | }
18 | Object.assign(freshNode, {
19 | type: COMMENT,
20 | prefix: '',
21 | hasSlashEnd: true,
22 | content: content,
23 | });
24 | return stringify(freshRules);
25 | };
26 |
27 | /**
28 | *
29 | */
30 | const stringifyAndHandleComments = (kids) => {
31 | return flatten(kids).join('');
32 | };
33 |
34 | /**
35 | *
36 | */
37 | const flatten = (kids, accumulator = []) => {
38 | for (const item of kids) {
39 | switch (item.type) {
40 | case ATRULE:
41 | case RULE:
42 | accumulator.push(handleInlineComments(item.selector) + (item.hasBraceBegin ? '{' : ''));
43 | if (item.kids && item.kids.length) {
44 | flatten(item.kids, accumulator);
45 | }
46 | accumulator.push((item.hasBraceEnd ? '}' : '') + (item.hasSemicolon ? ';' : ''));
47 | break;
48 | case DECLARATION:
49 | accumulator.push(
50 | handleInlineComments(item.property) +
51 | (item.hasColon ? ':' : '') +
52 | handleInlineComments(item.value) +
53 | (item.hasSemicolon ? ';' : '')
54 | );
55 | break;
56 | case COMMENT:
57 | accumulator.push(
58 | item.prefix +
59 | SLASH_SUBSTITUTE +
60 | '*' +
61 | item.content +
62 | (item.hasSlashEnd ? '*' + SLASH_SUBSTITUTE : '')
63 | );
64 | break;
65 | default:
66 | // nothing
67 | }
68 | }
69 | return accumulator;
70 | };
71 |
72 | /**
73 | *
74 | */
75 | const handleInlineComments = (blob) => {
76 | return blob
77 | .split('/*')
78 | .join(SLASH_SUBSTITUTE + '*')
79 | .split('*/')
80 | .join('*' + SLASH_SUBSTITUTE);
81 | };
82 |
83 | export default ignore;
84 |
--------------------------------------------------------------------------------
/src/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import cls from '../utils/cls';
4 | import stylize from '../utils/stylize';
5 |
6 | // =====================================================================================================================
7 | // D E C L A R A T I O N S
8 | // =====================================================================================================================
9 | const classes = stylize('Checkbox', {
10 | root: {
11 | position: 'relative',
12 | display: 'inline-block',
13 | verticalAlign: 'middle',
14 | marginTop: -2,
15 | marginRight: 4,
16 | width: 12,
17 | height: 12,
18 | border: 'solid 1px #333333',
19 | userSelect: 'none',
20 | },
21 | checked: {
22 | '&:after': {
23 | position: 'absolute',
24 | content: '""',
25 | left: 3,
26 | top: 0,
27 | width: 3,
28 | height: 7,
29 | border: 'solid 1px #000',
30 | borderTop: 'none',
31 | borderLeft: 'none',
32 | transform: 'rotate(45deg)',
33 | },
34 | },
35 | mixed: {
36 | // currently unused
37 | '&:after': {
38 | position: 'absolute',
39 | content: '""',
40 | left: 2,
41 | top: 2,
42 | width: 6,
43 | height: 6,
44 | background: '#333',
45 | },
46 | },
47 | });
48 |
49 | // =====================================================================================================================
50 | // C O M P O N E N T
51 | // =====================================================================================================================
52 | class Checkbox extends React.PureComponent {
53 | /**
54 | *
55 | */
56 | render() {
57 | const {tick} = this.props;
58 | return (
59 |
63 | );
64 | }
65 |
66 | /**
67 | *
68 | */
69 | onClick = (event) => {
70 | event.stopPropagation();
71 | const {onTick, id, tick} = this.props;
72 | onTick(id, [true, false, true][tick]); // 0 => true, 1 => false, 2 => true
73 | };
74 | }
75 |
76 | // =====================================================================================================================
77 | // D E F I N I T I O N
78 | // =====================================================================================================================
79 | Checkbox.defaultProps = {
80 | tick: 0,
81 | };
82 | export default Checkbox;
83 |
--------------------------------------------------------------------------------
/lib/utils/stylize.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.releaseStyling = exports.prepareStyling = exports["default"] = void 0;
7 |
8 | /*
9 | A quick-and-dirty simulation of JSS.
10 | */
11 | var PREFIX = 'rse';
12 | var SEPARATOR = '-';
13 |
14 | var dashConverter = function dashConverter(match) {
15 | return '-' + match.toLowerCase();
16 | };
17 |
18 | var registry = {};
19 | var cssCollection = [];
20 | var style = document.createElement('style');
21 | var count = 0;
22 | /**
23 | *
24 | */
25 |
26 | var stylize = function stylize(name, classes) {
27 | var output = {};
28 | var css = collect(name, classes, output);
29 | var index = registry[name];
30 |
31 | if (index === undefined) {
32 | registry[name] = cssCollection.push(css) - 1;
33 | } else {
34 | cssCollection[index] = css;
35 | }
36 |
37 | return output;
38 | };
39 | /**
40 | *
41 | */
42 |
43 |
44 | var collect = function collect(name, classes) {
45 | var accumulator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
46 | var css = '';
47 |
48 | for (var selector in classes) {
49 | var block = classes[selector];
50 | var className = PREFIX + SEPARATOR + name + SEPARATOR + selector;
51 | css += '.' + className + '{\r\n';
52 | var nested = {};
53 |
54 | for (var property in block) {
55 | var value = block[property];
56 |
57 | if (property.indexOf('&') >= 0) {
58 | // this is in fact a nested selector, not a declaration
59 | var resolved = property.replace(/&/g, selector);
60 | nested[resolved] = value;
61 | continue;
62 | }
63 |
64 | var cssProperty = property.replace(/([A-Z])/g, dashConverter);
65 | var cssValue = value + (typeof value === 'number' ? 'px' : '');
66 | css += ' ' + cssProperty + ':' + cssValue + ';\r\n';
67 | }
68 |
69 | css += '}\r\n';
70 |
71 | if (Object.keys(nested).length) {
72 | css += collect(name, nested);
73 | }
74 |
75 | accumulator[selector] = className;
76 | }
77 |
78 | return css;
79 | };
80 | /**
81 | *
82 | */
83 |
84 |
85 | var prepareStyling = function prepareStyling() {
86 | count++;
87 |
88 | if (count === 1) {
89 | // TODO: study impact on hot loading
90 | style.innerHTML = cssCollection.join('');
91 | document.head.appendChild(style);
92 | }
93 | };
94 | /**
95 | *
96 | */
97 |
98 |
99 | exports.prepareStyling = prepareStyling;
100 |
101 | var releaseStyling = function releaseStyling() {
102 | count--;
103 |
104 | if (count === 0) {
105 | document.head.removeChild(style);
106 | style.innerHTML = '';
107 | }
108 | };
109 |
110 | exports.releaseStyling = releaseStyling;
111 | var _default = stylize;
112 | exports["default"] = _default;
--------------------------------------------------------------------------------
/src/components/StyleEditor.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StyleEditor from './StyleEditor';
3 |
4 | const large = `
5 | div {
6 | background-color:red;
7 | back/*foo*/ground:blue;
8 | color:red; /* short comment*/
9 | overflow:hidden;
10 | /*border:none;*/
11 | foo: bar;
12 | font-weight:
13 | }
14 | /*span {color:red}*/
15 | @supports (display: flex) {
16 | @media screen and (min-width: 900px) {
17 | div {
18 | display: flex;
19 | }
20 | /*
21 | GIANT COMMENT:
22 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus aliquet est ut quam rutrum fringilla. Suspendisse et odio. Sed fringilla risus vel est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam auctor mi quis eros. Morbi justo nulla, lobortis imperdiet, consequat at, auctor ut, tortor. Sed est ipsum, posuere dictum, egestas ac, aliquam eu, sapien. Aenean ornare enim at mi. Phasellus id libero viverra felis elementum lobortis. Curabitur nec sapien gravida lacus lobortis tempor. Quisque eget mi a turpis rutrum venenatis. Nam tempus luctus nunc. Nulla ut orci ac est laoreet malesuada.
23 | */
24 | @media screen {
25 | div {
26 | color:lime;
27 | }
28 | }
29 | }
30 | }
31 | `;
32 |
33 | export default {
34 | component: StyleEditor,
35 | };
36 |
37 | export const Empty = {
38 | name: 'empty',
39 | };
40 | export const Large = {
41 | name: 'large',
42 | args: {
43 | defaultValue: large,
44 | },
45 | };
46 | export const Warning = {
47 | name: 'warning',
48 | args: {
49 | defaultValue: `@import 'custom.css';`,
50 | },
51 | };
52 | export const Height = {
53 | name: 'height forced',
54 | args: {
55 | defaultValue: 'div{color:red}',
56 | style: {height: 100},
57 | },
58 | };
59 | export const Invalid = {
60 | name: 'invalidRule',
61 | args: {
62 | defaultValue: '0div{mother:father;font-weight:bold}',
63 | },
64 | };
65 | export const Overwrite = {
66 | name: 'overwrite declarations',
67 | args: {
68 | defaultValue: 'div{background-color:red;background:blue;}',
69 | },
70 | };
71 | export const EmptySlots = {
72 | name: 'empty slots',
73 | args: {
74 | defaultValue: ' {mother:;: bold}',
75 | },
76 | };
77 | export const ReadOnly = {
78 | name: 'readOnly',
79 | args: {
80 | defaultValue: 'div{color:red}',
81 | readOnly: true,
82 | },
83 | };
84 | export const CommentsOutside = {
85 | name: 'comments outside',
86 | args: {
87 | defaultValue: '/*x*/h1{color:red} h2/*x*/{color:red} h3{color:red}/*x*/',
88 | },
89 | };
90 | export const CommentsInside = {
91 | name: 'comments inside',
92 | args: {
93 | defaultValue:
94 | 'h1{/*x*/color:red} h2{c/*x*/olor:red} h3{color:/*x*/red} h4{color:red/*x*/} h5{color:red;/*x*/} h6{color:red;/*x*//*x*/} .empty{color:red;/**//**//**//**/}',
95 | },
96 | };
97 |
--------------------------------------------------------------------------------
/tests/ignore.test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
38 |
39 |
40 |
41 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/lib/utils/stringify.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports["default"] = void 0;
7 |
8 | var _COMMON = require("./COMMON.js");
9 |
10 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
11 |
12 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
13 |
14 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
15 |
16 | /**
17 | *
18 | */
19 | var stringify = function stringify(kids) {
20 | return flatten(kids).join('');
21 | };
22 | /**
23 | *
24 | */
25 |
26 |
27 | var flatten = function flatten(kids) {
28 | var accumulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
29 |
30 | var _iterator = _createForOfIteratorHelper(kids),
31 | _step;
32 |
33 | try {
34 | for (_iterator.s(); !(_step = _iterator.n()).done;) {
35 | var item = _step.value;
36 |
37 | switch (item.type) {
38 | case _COMMON.ATRULE:
39 | case _COMMON.RULE:
40 | accumulator.push(item.selector + (item.hasBraceBegin ? '{' : ''));
41 |
42 | if (item.kids && item.kids.length) {
43 | flatten(item.kids, accumulator);
44 | }
45 |
46 | accumulator.push((item.hasBraceEnd ? '}' : '') + (item.hasSemicolon ? ';' : ''));
47 | break;
48 |
49 | case _COMMON.DECLARATION:
50 | accumulator.push(item.property + (item.hasColon ? ':' : '') + item.value + (item.hasSemicolon ? ';' : ''));
51 | break;
52 |
53 | case _COMMON.COMMENT:
54 | accumulator.push(item.prefix + '/*' + item.content + (item.hasSlashEnd ? '*/' : ''));
55 | break;
56 |
57 | default: // nothing
58 |
59 | }
60 | }
61 | } catch (err) {
62 | _iterator.e(err);
63 | } finally {
64 | _iterator.f();
65 | }
66 |
67 | return accumulator;
68 | };
69 |
70 | var _default = stringify;
71 | exports["default"] = _default;
--------------------------------------------------------------------------------
/lib/utils/identify.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports["default"] = void 0;
7 |
8 | var _COMMON = require("./COMMON.js");
9 |
10 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
11 |
12 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
13 |
14 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
15 |
16 | var MAX_CHARS = 32; // how many characters to use as identifier. Protects against giant base64.
17 |
18 | /**
19 | *
20 | */
21 |
22 | var identify = function identify(list) {
23 | var usedIds = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
24 |
25 | var _iterator = _createForOfIteratorHelper(list),
26 | _step;
27 |
28 | try {
29 | for (_iterator.s(); !(_step = _iterator.n()).done;) {
30 | var item = _step.value;
31 | var id = void 0;
32 |
33 | switch (item.type) {
34 | case _COMMON.ATRULE:
35 | case _COMMON.RULE:
36 | id = item.selector.trim() + (item.hasBraceBegin ? '{' : '') + (item.hasSemicolon ? ';' : '');
37 | break;
38 |
39 | case _COMMON.DECLARATION:
40 | id = item.property.trim() + (item.hasColon ? ':' : '') + item.value.substr(0, MAX_CHARS) + (item.hasSemicolon ? ';' : '');
41 | break;
42 |
43 | case _COMMON.COMMENT:
44 | id = '/*' + item.content.substr(0, MAX_CHARS) + '*/';
45 | break;
46 |
47 | default: // nothing
48 |
49 | }
50 |
51 | if (id in usedIds) {
52 | usedIds[id]++;
53 | item.id = id + usedIds[id];
54 | } else {
55 | usedIds[id] = 1;
56 | item.id = id;
57 | }
58 |
59 | if (item.kids && item.kids.length) {
60 | identify(item.kids, usedIds);
61 | }
62 | }
63 | } catch (err) {
64 | _iterator.e(err);
65 | } finally {
66 | _iterator.f();
67 | }
68 | };
69 |
70 | var _default = identify;
71 | exports["default"] = _default;
--------------------------------------------------------------------------------
/lib/utils/unignore.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports["default"] = void 0;
7 |
8 | var _COMMON = require("./COMMON.js");
9 |
10 | var _modify2 = _interopRequireDefault(require("./modify.js"));
11 |
12 | var _stringify = _interopRequireDefault(require("./stringify.js"));
13 |
14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
15 |
16 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
17 |
18 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
19 |
20 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
21 |
22 | /**
23 | *
24 | */
25 | var unignore = function unignore(rules, id) {
26 | var _modify = (0, _modify2["default"])(rules, id, {}),
27 | freshRules = _modify.freshRules,
28 | freshNode = _modify.freshNode; // blank change to get the `freshNode`
29 |
30 |
31 | if (freshNode.type === _COMMON.COMMENT) {
32 | unignoreComment(freshNode);
33 | } else {
34 | unignoreKids(freshNode.kids);
35 | }
36 |
37 | return (0, _stringify["default"])(freshRules);
38 | };
39 | /**
40 | *
41 | */
42 |
43 |
44 | var unignoreComment = function unignoreComment(node) {
45 | var prefix = node.prefix; // backup
46 |
47 | var content = node.content.split(_COMMON.SLASH_SUBSTITUTE + '*').join('/*').split('*' + _COMMON.SLASH_SUBSTITUTE).join('*/');
48 |
49 | for (var key in node) {
50 | delete node[key];
51 | }
52 |
53 | Object.assign(node, {
54 | type: _COMMON.RULE,
55 | // could also be ATRULE or DECLARATION, because it's just temporary
56 | selector: prefix + content
57 | });
58 | };
59 | /**
60 | *
61 | */
62 |
63 |
64 | var unignoreKids = function unignoreKids(kids) {
65 | var _iterator = _createForOfIteratorHelper(kids),
66 | _step;
67 |
68 | try {
69 | for (_iterator.s(); !(_step = _iterator.n()).done;) {
70 | var item = _step.value;
71 |
72 | if (item.type === _COMMON.COMMENT) {
73 | unignoreComment(item);
74 | } else {
75 | if (item.kids && item.kids.length) {
76 | unignoreKids(item.kids);
77 | }
78 | }
79 | }
80 | } catch (err) {
81 | _iterator.e(err);
82 | } finally {
83 | _iterator.f();
84 | }
85 | };
86 |
87 | var _default = unignore;
88 | exports["default"] = _default;
--------------------------------------------------------------------------------
/lib/utils/prettify.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports["default"] = void 0;
7 |
8 | var _COMMON = require("./COMMON");
9 |
10 | var _clean = _interopRequireDefault(require("./clean"));
11 |
12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
13 |
14 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
15 |
16 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
17 |
18 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
19 |
20 | /**
21 | *
22 | */
23 | var prettify = function prettify(kids) {
24 | return flatten(kids).join('');
25 | };
26 | /**
27 | *
28 | */
29 |
30 |
31 | var flatten = function flatten(kids) {
32 | var accumulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
33 | var indent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
34 |
35 | var _iterator = _createForOfIteratorHelper(kids),
36 | _step;
37 |
38 | try {
39 | for (_iterator.s(); !(_step = _iterator.n()).done;) {
40 | var item = _step.value;
41 |
42 | switch (item.type) {
43 | case _COMMON.ATRULE:
44 | case _COMMON.RULE:
45 | var type = item.type,
46 | _kids = item.kids,
47 | selector = item.selector,
48 | hasBraceBegin = item.hasBraceBegin,
49 | hasBraceEnd = item.hasBraceEnd,
50 | hasSemicolon = item.hasSemicolon;
51 |
52 | if (!_kids.length && !selector.trim() && !hasBraceBegin && !hasBraceEnd && !hasSemicolon) {
53 | continue;
54 | }
55 |
56 | accumulator.push(indent + (0, _clean["default"])(selector) + ' {\r\n');
57 |
58 | if (_kids && _kids.length) {
59 | flatten(_kids, accumulator, indent + ' ');
60 | }
61 |
62 | if (type === _COMMON.ATRULE && !hasBraceBegin) {
63 | accumulator.push(';\r\n');
64 | } else {
65 | accumulator.push(indent + '}\r\n');
66 | }
67 |
68 | break;
69 |
70 | case _COMMON.DECLARATION:
71 | if (!item.hasColon && !item.property.trim()) {
72 | continue;
73 | }
74 |
75 | accumulator.push(indent + (0, _clean["default"])(item.property) + ': ' + (0, _clean["default"])(item.value) + ';\r\n');
76 | break;
77 |
78 | case _COMMON.COMMENT:
79 | accumulator.push(indent + '/*' + item.content + '*/\r\n');
80 | break;
81 |
82 | default: // nothing
83 |
84 | }
85 | }
86 | } catch (err) {
87 | _iterator.e(err);
88 | } finally {
89 | _iterator.f();
90 | }
91 |
92 | return accumulator;
93 | };
94 |
95 | var _default = prettify;
96 | exports["default"] = _default;
--------------------------------------------------------------------------------
/tests/validate.test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
36 |
37 |
38 |
39 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/tests/parse.test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
36 |
37 |
38 |
39 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | import {ATRULE, COMMENT} from './COMMON';
5 |
6 | let sheet;
7 | const BASE64_TEMP = ';base64,0';
8 | const base64Pattern = /;base64,[a-zA-Z/0-9+=]*/g;
9 |
10 | /**
11 | *
12 | */
13 | const validate = (list) => {
14 | sheet = sheet || createPlayground(); // lazy initialization
15 | validateRules(list, '', '', '');
16 | return list;
17 | };
18 |
19 | /**
20 | *
21 | */
22 | const validateRules = (list, parentPrefix, parentSuffix, parentFingerprint) => {
23 | for (const rule of list) {
24 | if (rule.type === COMMENT) {
25 | continue;
26 | }
27 | const adaptedSelector = rule.selector.split('&').join('#x'); // act as if `&` is valid
28 | const rulePrefix = parentPrefix + adaptedSelector + (rule.hasBraceBegin ? '{' : '');
29 | const ruleSuffix = (rule.hasBraceEnd ? '}' : '') + (rule.hasSemicolon ? ';' : '') + parentSuffix;
30 | const fingerprint = inAndOut(rulePrefix + ruleSuffix);
31 | if (fingerprint !== parentFingerprint) {
32 | // the browser accepted our rule
33 | rule.isValid = true;
34 | if (rule.kids.length) {
35 | if (rule.type === ATRULE) {
36 | validateRules(rule.kids, rulePrefix, ruleSuffix, fingerprint);
37 | } else {
38 | // RULE
39 | validateDeclarations(rule.kids, rulePrefix, ruleSuffix, fingerprint);
40 | }
41 | }
42 | } else {
43 | rule.isValid = false;
44 | if (rule.kids.length) {
45 | invalidateChildren(rule.kids);
46 | }
47 | }
48 | }
49 | };
50 |
51 | /**
52 | *
53 | */
54 | const validateDeclarations = (list, parentPrefix, parentSuffix, parentFingerprint) => {
55 | let fingerprint = parentFingerprint;
56 | let block = '';
57 | for (let i = list.length - 1; i >= 0; i--) {
58 | // we traverse backwards to detect overruled declarations
59 | const declaration = list[i];
60 | if (declaration.type === COMMENT) {
61 | continue;
62 | }
63 | block = (declaration.hasSemicolon ? ';' : '') + block;
64 | const safeDeclarationValue = declaration.value.replace(base64Pattern, BASE64_TEMP);
65 | block = declaration.property + (declaration.hasColon ? ':' : '') + safeDeclarationValue + block;
66 | const freshFingerprint = inAndOut(parentPrefix + block + parentSuffix);
67 | if (fingerprint !== freshFingerprint) {
68 | // the browser accepted our declaration
69 | declaration.isValid = true;
70 | fingerprint = freshFingerprint;
71 | } else {
72 | declaration.isValid = false;
73 | }
74 | }
75 | };
76 |
77 | /**
78 | *
79 | */
80 | const invalidateChildren = (list) => {
81 | for (const item of list) {
82 | if (item.type === COMMENT) {
83 | continue;
84 | }
85 | item.isValid = false;
86 | const kids = item.kids;
87 | if (kids && kids.length) {
88 | invalidateChildren(kids);
89 | }
90 | }
91 | };
92 |
93 | /**
94 | *
95 | */
96 | const inAndOut = (blob) => {
97 | let index;
98 | try {
99 | index = sheet.insertRule(blob);
100 | } catch (e) {
101 | // console.log(e);
102 | }
103 | if (index >= 0) {
104 | const fingerprint = sheet.cssRules[index].cssText;
105 | sheet.deleteRule(index);
106 | return fingerprint;
107 | }
108 | return '';
109 | };
110 |
111 | /**
112 | *
113 | * Note: DocumentFragment doesn't work because it doesn't compute styles.
114 | */
115 | const createPlayground = () => {
116 | const iframe = document.createElement('iframe');
117 | iframe.style.display = 'none';
118 | document.head.appendChild(iframe);
119 | const iframeDocument = iframe.contentWindow.document;
120 | const style = iframeDocument.createElement('style');
121 | iframeDocument.head.appendChild(style);
122 |
123 | // Important: Since Chrome 80 (or so), we need to remove the iframe AFTER we added the style.
124 | document.head.removeChild(iframe);
125 |
126 | return style.sheet;
127 | };
128 |
129 | /**
130 | *
131 | */
132 | const destroyPlayground = () => {
133 | sheet = null;
134 | };
135 |
136 | export default validate;
137 | export {destroyPlayground};
138 |
--------------------------------------------------------------------------------
/lib/utils/ignore.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports["default"] = void 0;
7 |
8 | var _modify2 = _interopRequireDefault(require("./modify.js"));
9 |
10 | var _stringify = _interopRequireDefault(require("./stringify.js"));
11 |
12 | var _COMMON = require("./COMMON.js");
13 |
14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
15 |
16 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
17 |
18 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
19 |
20 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
21 |
22 | /**
23 | *
24 | */
25 | var ignore = function ignore(oldRules, id) {
26 | var _modify = (0, _modify2["default"])(oldRules, id, {}),
27 | freshRules = _modify.freshRules,
28 | freshNode = _modify.freshNode; // blank change to get the `freshNode`
29 |
30 |
31 | var content = stringifyAndHandleComments([freshNode]);
32 |
33 | for (var key in freshNode) {
34 | delete freshNode[key];
35 | }
36 |
37 | Object.assign(freshNode, {
38 | type: _COMMON.COMMENT,
39 | prefix: '',
40 | hasSlashEnd: true,
41 | content: content
42 | });
43 | return (0, _stringify["default"])(freshRules);
44 | };
45 | /**
46 | *
47 | */
48 |
49 |
50 | var stringifyAndHandleComments = function stringifyAndHandleComments(kids) {
51 | return flatten(kids).join('');
52 | };
53 | /**
54 | *
55 | */
56 |
57 |
58 | var flatten = function flatten(kids) {
59 | var accumulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
60 |
61 | var _iterator = _createForOfIteratorHelper(kids),
62 | _step;
63 |
64 | try {
65 | for (_iterator.s(); !(_step = _iterator.n()).done;) {
66 | var item = _step.value;
67 |
68 | switch (item.type) {
69 | case _COMMON.ATRULE:
70 | case _COMMON.RULE:
71 | accumulator.push(handleInlineComments(item.selector) + (item.hasBraceBegin ? '{' : ''));
72 |
73 | if (item.kids && item.kids.length) {
74 | flatten(item.kids, accumulator);
75 | }
76 |
77 | accumulator.push((item.hasBraceEnd ? '}' : '') + (item.hasSemicolon ? ';' : ''));
78 | break;
79 |
80 | case _COMMON.DECLARATION:
81 | accumulator.push(handleInlineComments(item.property) + (item.hasColon ? ':' : '') + handleInlineComments(item.value) + (item.hasSemicolon ? ';' : ''));
82 | break;
83 |
84 | case _COMMON.COMMENT:
85 | accumulator.push(item.prefix + _COMMON.SLASH_SUBSTITUTE + '*' + item.content + (item.hasSlashEnd ? '*' + _COMMON.SLASH_SUBSTITUTE : ''));
86 | break;
87 |
88 | default: // nothing
89 |
90 | }
91 | }
92 | } catch (err) {
93 | _iterator.e(err);
94 | } finally {
95 | _iterator.f();
96 | }
97 |
98 | return accumulator;
99 | };
100 | /**
101 | *
102 | */
103 |
104 |
105 | var handleInlineComments = function handleInlineComments(blob) {
106 | return blob.split('/*').join(_COMMON.SLASH_SUBSTITUTE + '*').split('*/').join('*' + _COMMON.SLASH_SUBSTITUTE);
107 | };
108 |
109 | var _default = ignore;
110 | exports["default"] = _default;
--------------------------------------------------------------------------------
/src/components/Comment.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import stylize from '../utils/stylize';
4 | import clean from '../utils/clean';
5 | import Checkbox from './Checkbox';
6 | import Area from './Area';
7 | import {AFTER} from '../utils/COMMON';
8 | import shorten from '../utils/shorten';
9 | import hasSelection from '../utils/hasSelection';
10 |
11 | // =====================================================================================================================
12 | // D E C L A R A T I O N S
13 | // =====================================================================================================================
14 | const classes = stylize('Comment', {
15 | root: {
16 | color: 'silver',
17 | padding: '2px 0',
18 | whiteSpace: 'nowrap',
19 | overflow: 'hidden',
20 | textOverflow: 'ellipsis',
21 | },
22 | content: {
23 | cursor: 'text',
24 | borderBottom: '1px dashed transparent',
25 | '&:hover': {
26 | borderBottomColor: 'currentColor',
27 | },
28 | },
29 | after: {
30 | marginTop: 4,
31 | },
32 | });
33 |
34 | const MAX_CHARS_VALUE = 32; // how many characters to display in the value. Protects against giant base64.
35 | const MAX_CHARS_TITLE = 512; // how many characters to display in the tooltip. Protects against giant base64.
36 |
37 | // =====================================================================================================================
38 | // C O M P O N E N T
39 | // =====================================================================================================================
40 | class Comment extends React.PureComponent {
41 | state = {
42 | isEditingContent: false,
43 | isEditingAfter: false,
44 | };
45 |
46 | /**
47 | *
48 | */
49 | render() {
50 | const {id, content, onTick} = this.props;
51 | const {isEditingContent, isEditingAfter} = this.state;
52 |
53 | const cleanContent = clean(content);
54 |
55 | let shortContent = cleanContent;
56 | let shortTitle = '';
57 | if (cleanContent.length > MAX_CHARS_VALUE) {
58 | shortContent = shorten(cleanContent, MAX_CHARS_VALUE);
59 | shortTitle = shorten(cleanContent, MAX_CHARS_TITLE);
60 | }
61 |
62 | return (
63 |
64 |
65 |
66 | {isEditingContent ? (
67 | this.renderArea('content', content)
68 | ) : (
69 |
70 | {'/*' + shortContent + '*/'}
71 |
72 | )}
73 | {isEditingAfter && (
74 |
75 |
76 | {this.renderArea(AFTER, '')}
77 |
78 | )}
79 |
80 | );
81 | }
82 |
83 | /**
84 | *
85 | */
86 | renderArea = (payloadProperty, defaultValue) => {
87 | const {id, onEditChange} = this.props;
88 | return (
89 |
96 | );
97 | };
98 |
99 | /**
100 | *
101 | */
102 | onContentClick = (event) => {
103 | if (hasSelection()) return;
104 | event.stopPropagation();
105 | this.setState({isEditingContent: true});
106 | this.props.onEditBegin();
107 | };
108 |
109 | /**
110 | *
111 | */
112 | onCommentClick = (event) => {
113 | if (hasSelection()) return;
114 | event.stopPropagation();
115 | this.setState({isEditingAfter: true});
116 | this.props.onEditBegin();
117 | };
118 |
119 | /**
120 | *
121 | */
122 | onAreaBlur = (id, payload) => {
123 | this.setState({
124 | isEditingContent: false,
125 | isEditingAfter: false,
126 | });
127 | this.props.onEditEnd(id, payload);
128 | };
129 | }
130 |
131 | // =====================================================================================================================
132 | // D E F I N I T I O N
133 | // =====================================================================================================================
134 | export default Comment;
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Style Editor
2 |
3 | [![Npm Version][npm-version-image]][npm-version-url] [![Size][bundlephobia-image]][bundlephobia-url]
4 |
5 | A React component that displays and edits CSS, similar to the browser's DevTools.
6 |
7 | [](https://aurelain.github.io/react-style-editor/)
8 |
9 | ## [Live demo](https://aurelain.github.io/react-style-editor/)
10 |
11 | ## Features
12 |
13 | - Parses any CSS string and formats it in a familiar fashion
14 | - Validates each rule and each declaration using the browsers's own engine
15 | - Facilitates commenting the CSS code through checkbox toggling
16 | - Allows easy additions by clicking next to the desired location
17 | - Has no dependencies (other than React)
18 | - Is tiny (< 10 KB minified)
19 | - Is customizable through classes
20 | - Offers 3 output formats:
21 | - the code with preserved formatting
22 | - a machine-friendly model of the code (recursive array of objects)
23 | - the prettified code
24 |
25 | ## Installation
26 |
27 | ```sh
28 | npm i react-style-editor
29 | ```
30 |
31 | ## Usage
32 |
33 | ```js
34 | import React from 'react';
35 | import StyleEditor from 'react-style-editor';
36 |
37 | class Component extends React.Component {
38 | render() {
39 | return (
40 |
51 | );
52 | }
53 | }
54 | ```
55 |
56 | ## Props
57 |
58 | | prop | type | default | description |
59 | | --------------- | -------- | ----------- | --------------------------------------------------------------------------------------------------------- |
60 | | `defaultValue` | string | `''` | The initial CSS code |
61 | | `value` | string | `undefined` | The controlled CSS code |
62 | | `onChange` | function | `null` | A closure that receives a single argument, `string` or `array`, depending on the value of `outputFormats` |
63 | | `outputFormats` | string | `'pretty'` | Comma-separated values of: `'preserved'`, `'machine'`, `'pretty'` |
64 | | `readOnly` | boolean | `false` | All interactions with the component are blocked |
65 |
66 | 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)).
67 | A short summary:
68 |
69 | - `defaultValue` => uncontrolled, the component is on its own
70 | - `value` => controlled => you must also use the `onChange` or `readOnly` properties.
71 |
72 | The above behavior is identical to that of normal React form elements, e.g. ``.
73 |
74 | Any other props are spread to the internal root.
75 |
76 | ## Exports
77 |
78 | Besides the default export (`StyleEditor`), there are also a few utility functions exported:
79 |
80 | - `analyze()`: ouputs the `machine` format
81 | - `parse()`: a lighter version of `analyze()`
82 | - `stringify()`: outputs the `preserved` format
83 | - `prettify()`: outputs the `pretty` format
84 |
85 | They all expect a CSS string as parameter and are useful if you don't want to use the React component and wait for its `onChange`.
86 |
87 | ## Wishlist
88 |
89 | - Color swatches (similar to the browser)
90 | - Dropdown suggestions for properties/values (similar to the browser)
91 | - Keyboard support for `TAB`, `:` and `UP`/`DOWN` increments of numeric values
92 | - Theme support (similar to the browser)
93 | - Toggle view mode: tree/original
94 | - Undo/redo
95 | - Better code quality through `propTypes`
96 | - Filters (similar to the browser)
97 | - Error messages displayed in the warning-sign's tooltip
98 |
99 | [npm-version-image]: https://img.shields.io/npm/v/react-style-editor.svg?style=flat-square
100 | [npm-version-url]: https://www.npmjs.com/package/react-style-editor
101 | [bundlephobia-image]: https://img.shields.io/bundlephobia/minzip/react-style-editor.svg?style=flat-square
102 | [bundlephobia-url]: https://bundlephobia.com/result?p=react-style-editor
103 |
--------------------------------------------------------------------------------
/tests/database.test.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
29 |
30 |
31 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/src/components/Area.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import stylize from '../utils/stylize';
4 |
5 | // =====================================================================================================================
6 | // D E C L A R A T I O N S
7 | // =====================================================================================================================
8 | const classes = stylize('Area', {
9 | root: {
10 | fontFamily: 'Consolas, Liberation Mono, Menlo, monospace', // synced with StyleEditor's fontFamily
11 | fontSize: '12px', // synced with StyleEditor's fontSize
12 | resize: 'none',
13 | verticalAlign: 'middle',
14 | overflow: 'hidden',
15 | padding: 2,
16 | border: 'none',
17 | outline: 'solid 1px #ccc',
18 | outlineOffset: '-1px',
19 | minWidth: 4,
20 | textDecoration: 'none', // to combat `isInvalid` from upstream
21 | pointerEvents: 'auto !important', // to combat the general lock imposed by StyleEditor
22 | },
23 | });
24 |
25 | // =====================================================================================================================
26 | // C O M P O N E N T
27 | // =====================================================================================================================
28 | class Area extends React.PureComponent {
29 | ref = React.createRef();
30 |
31 | /**
32 | *
33 | */
34 | render() {
35 | const {defaultValue} = this.props;
36 | return (
37 |
47 | );
48 | }
49 |
50 | componentDidMount() {
51 | const textarea = this.ref.current;
52 | textarea.focus();
53 | textarea.select();
54 | this.autoSize();
55 | }
56 |
57 | /**
58 | *
59 | */
60 | autoSize = () => {
61 | const textarea = this.ref.current;
62 | textarea.style.whiteSpace = 'noWrap';
63 | textarea.style.width = '0';
64 | textarea.style.height = '0';
65 | const w = textarea.scrollWidth;
66 |
67 | const previousElement = textarea.previousElementSibling;
68 | let offset = 0;
69 | if (previousElement) {
70 | offset = previousElement.offsetLeft + previousElement.offsetWidth;
71 | }
72 |
73 | if (offset + w > textarea.parentNode.offsetWidth) {
74 | textarea.style.whiteSpace = 'normal';
75 | textarea.style.display = 'block';
76 | textarea.style.width = '100%';
77 | } else {
78 | textarea.style.display = 'inline-block';
79 | textarea.style.width = textarea.scrollWidth + 2 + 'px';
80 | }
81 | textarea.style.height = textarea.scrollHeight + 'px';
82 | };
83 |
84 | /**
85 | *
86 | */
87 | onClick = (event) => {
88 | event.stopPropagation(); // prevent upstream handlers that would cause a blur
89 | };
90 |
91 | /**
92 | *
93 | */
94 | onChange = (event) => {
95 | event.stopPropagation(); // we're handling the change manually and this event collides with us upstream
96 | this.autoSize();
97 | const {onChange, id, payloadProperty} = this.props;
98 | onChange(id, {[payloadProperty]: event.currentTarget.value});
99 | };
100 |
101 | /**
102 | *
103 | */
104 | onBlur = (event) => {
105 | const {onBlur, id, payloadProperty} = this.props;
106 | onBlur(id, {[payloadProperty]: event.currentTarget.value});
107 | };
108 |
109 | /**
110 | *
111 | */
112 | onKeyDown = (event) => {
113 | // console.log(event.key);
114 | switch (event.key) {
115 | case 'Enter':
116 | if (event.shiftKey) {
117 | return; // allow Shift+Enter
118 | }
119 | this.onBlur(event);
120 | cancelEvent(event);
121 | return;
122 | case 'Escape':
123 | event.currentTarget.value = this.props.defaultValue;
124 | this.onChange(event);
125 | this.onBlur(event);
126 | cancelEvent(event);
127 | break;
128 | default:
129 | break; // allow any other characters
130 | }
131 | };
132 | }
133 |
134 | // =====================================================================================================================
135 | // H E L P E R S
136 | // =====================================================================================================================
137 | /**
138 | *
139 | */
140 | const cancelEvent = (event) => {
141 | event.preventDefault();
142 | event.stopPropagation();
143 | };
144 |
145 | // =====================================================================================================================
146 | // D E F I N I T I O N
147 | // =====================================================================================================================
148 | export default Area;
149 |
--------------------------------------------------------------------------------
/lib/utils/validate.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports.destroyPlayground = exports["default"] = void 0;
7 |
8 | var _COMMON = require("./COMMON");
9 |
10 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
11 |
12 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
13 |
14 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
15 |
16 | var sheet;
17 | var BASE64_TEMP = ';base64,0';
18 | var base64Pattern = /;base64,[a-zA-Z/0-9+=]*/g;
19 | /**
20 | *
21 | */
22 |
23 | var validate = function validate(list) {
24 | sheet = sheet || createPlayground(); // lazy initialization
25 |
26 | validateRules(list, '', '', '');
27 | return list;
28 | };
29 | /**
30 | *
31 | */
32 |
33 |
34 | var validateRules = function validateRules(list, parentPrefix, parentSuffix, parentFingerprint) {
35 | var _iterator = _createForOfIteratorHelper(list),
36 | _step;
37 |
38 | try {
39 | for (_iterator.s(); !(_step = _iterator.n()).done;) {
40 | var rule = _step.value;
41 |
42 | if (rule.type === _COMMON.COMMENT) {
43 | continue;
44 | }
45 |
46 | var adaptedSelector = rule.selector.split('&').join('#x'); // act as if `&` is valid
47 |
48 | var rulePrefix = parentPrefix + adaptedSelector + (rule.hasBraceBegin ? '{' : '');
49 | var ruleSuffix = (rule.hasBraceEnd ? '}' : '') + (rule.hasSemicolon ? ';' : '') + parentSuffix;
50 | var fingerprint = inAndOut(rulePrefix + ruleSuffix);
51 |
52 | if (fingerprint !== parentFingerprint) {
53 | // the browser accepted our rule
54 | rule.isValid = true;
55 |
56 | if (rule.kids.length) {
57 | if (rule.type === _COMMON.ATRULE) {
58 | validateRules(rule.kids, rulePrefix, ruleSuffix, fingerprint);
59 | } else {
60 | // RULE
61 | validateDeclarations(rule.kids, rulePrefix, ruleSuffix, fingerprint);
62 | }
63 | }
64 | } else {
65 | rule.isValid = false;
66 |
67 | if (rule.kids.length) {
68 | invalidateChildren(rule.kids);
69 | }
70 | }
71 | }
72 | } catch (err) {
73 | _iterator.e(err);
74 | } finally {
75 | _iterator.f();
76 | }
77 | };
78 | /**
79 | *
80 | */
81 |
82 |
83 | var validateDeclarations = function validateDeclarations(list, parentPrefix, parentSuffix, parentFingerprint) {
84 | var fingerprint = parentFingerprint;
85 | var block = '';
86 |
87 | for (var i = list.length - 1; i >= 0; i--) {
88 | // we traverse backwards to detect overruled declarations
89 | var declaration = list[i];
90 |
91 | if (declaration.type === _COMMON.COMMENT) {
92 | continue;
93 | }
94 |
95 | block = (declaration.hasSemicolon ? ';' : '') + block;
96 | var safeDeclarationValue = declaration.value.replace(base64Pattern, BASE64_TEMP);
97 | block = declaration.property + (declaration.hasColon ? ':' : '') + safeDeclarationValue + block;
98 | var freshFingerprint = inAndOut(parentPrefix + block + parentSuffix);
99 |
100 | if (fingerprint !== freshFingerprint) {
101 | // the browser accepted our declaration
102 | declaration.isValid = true;
103 | fingerprint = freshFingerprint;
104 | } else {
105 | declaration.isValid = false;
106 | }
107 | }
108 | };
109 | /**
110 | *
111 | */
112 |
113 |
114 | var invalidateChildren = function invalidateChildren(list) {
115 | var _iterator2 = _createForOfIteratorHelper(list),
116 | _step2;
117 |
118 | try {
119 | for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
120 | var item = _step2.value;
121 |
122 | if (item.type === _COMMON.COMMENT) {
123 | continue;
124 | }
125 |
126 | item.isValid = false;
127 | var kids = item.kids;
128 |
129 | if (kids && kids.length) {
130 | invalidateChildren(kids);
131 | }
132 | }
133 | } catch (err) {
134 | _iterator2.e(err);
135 | } finally {
136 | _iterator2.f();
137 | }
138 | };
139 | /**
140 | *
141 | */
142 |
143 |
144 | var inAndOut = function inAndOut(blob) {
145 | var index;
146 |
147 | try {
148 | index = sheet.insertRule(blob);
149 | } catch (e) {// console.log(e);
150 | }
151 |
152 | if (index >= 0) {
153 | var fingerprint = sheet.cssRules[index].cssText;
154 | sheet.deleteRule(index);
155 | return fingerprint;
156 | }
157 |
158 | return '';
159 | };
160 | /**
161 | *
162 | * Note: DocumentFragment doesn't work because it doesn't compute styles.
163 | */
164 |
165 |
166 | var createPlayground = function createPlayground() {
167 | var iframe = document.createElement('iframe');
168 | iframe.style.display = 'none';
169 | document.head.appendChild(iframe);
170 | var iframeDocument = iframe.contentWindow.document;
171 | var style = iframeDocument.createElement('style');
172 | iframeDocument.head.appendChild(style); // Important: Since Chrome 80 (or so), we need to remove the iframe AFTER we added the style.
173 |
174 | document.head.removeChild(iframe);
175 | return style.sheet;
176 | };
177 | /**
178 | *
179 | */
180 |
181 |
182 | var destroyPlayground = function destroyPlayground() {
183 | sheet = null;
184 | };
185 |
186 | exports.destroyPlayground = destroyPlayground;
187 | var _default = validate;
188 | exports["default"] = _default;
--------------------------------------------------------------------------------
/src/components/Declaration.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import cls from '../utils/cls';
4 | import stylize from '../utils/stylize';
5 | import clean from '../utils/clean';
6 | import shorten from '../utils/shorten';
7 | import Checkbox from './Checkbox';
8 | import Area from './Area';
9 | import {AFTER} from '../utils/COMMON';
10 | import Alert from './Alert';
11 | import hasSelection from '../utils/hasSelection';
12 |
13 | // =====================================================================================================================
14 | // D E C L A R A T I O N S
15 | // =====================================================================================================================
16 | const classes = stylize('Declaration', {
17 | root: {
18 | padding: '2px 0',
19 | whiteSpace: 'nowrap',
20 | overflow: 'hidden',
21 | textOverflow: 'ellipsis',
22 | },
23 | property: {
24 | color: 'rgb(0, 116, 232)', // Firefox
25 | cursor: 'text',
26 | borderBottom: '1px dashed transparent',
27 | '&:hover': {
28 | borderBottomColor: 'currentColor',
29 | },
30 | },
31 | value: {
32 | color: 'rgb(221, 0, 169)', // Firefox
33 | cursor: 'text',
34 | borderBottom: '1px dashed transparent',
35 | '&:hover': {
36 | borderBottomColor: 'currentColor',
37 | },
38 | },
39 | isEmpty: {
40 | padding: '0 6px',
41 | background: '#eee',
42 | '&:hover': {
43 | background: '#ddd',
44 | },
45 | },
46 | after: {
47 | marginTop: 6,
48 | },
49 | isInvalid: {
50 | textDecoration: 'line-through',
51 | textDecorationColor: '#939395',
52 | },
53 | });
54 |
55 | const MAX_CHARS_VALUE = 32; // how many characters to display in the value. Protects against giant base64.
56 | const MAX_CHARS_TITLE = 512; // how many characters to display in the tooltip. Protects against giant base64.
57 |
58 | // =====================================================================================================================
59 | // C O M P O N E N T
60 | // =====================================================================================================================
61 | class Declaration extends React.PureComponent {
62 | state = {
63 | isEditingProperty: false,
64 | isEditingValue: false,
65 | isEditingAfter: false,
66 | };
67 |
68 | /**
69 | *
70 | */
71 | render() {
72 | const {id, property, value, hasColon, onTick, isValid} = this.props;
73 | const {isEditingProperty, isEditingValue, isEditingAfter} = this.state;
74 |
75 | if (!hasColon && !property.trim()) {
76 | return null;
77 | }
78 | const cleanProperty = clean(property);
79 | const cleanValue = clean(value);
80 |
81 | let shortValue = cleanValue;
82 | let shortTitle = '';
83 | if (cleanValue.length > MAX_CHARS_VALUE) {
84 | shortValue = shorten(cleanValue, MAX_CHARS_VALUE);
85 | shortTitle = shorten(cleanValue, MAX_CHARS_TITLE);
86 | }
87 |
88 | return (
89 |
90 |
91 |
92 | {isEditingProperty ? (
93 | this.renderArea('property', property)
94 | ) : (
95 |
101 | {cleanProperty}
102 |
103 | )}
104 |
105 | {': '}
106 |
107 | {isEditingValue ? (
108 | this.renderArea('value', value)
109 | ) : (
110 |
117 | {shortValue}
118 |
119 | )}
120 |
121 | {';'}
122 |
123 | {!isValid &&
}
124 |
125 | {isEditingAfter && (
126 |
127 |
128 | {this.renderArea(AFTER, '')}
129 |
130 | )}
131 |
132 | );
133 | }
134 |
135 | /**
136 | *
137 | */
138 | renderArea = (payloadProperty, defaultValue) => {
139 | const {id, onEditChange} = this.props;
140 | return (
141 |
148 | );
149 | };
150 |
151 | /**
152 | *
153 | */
154 | onDeclarationClick = (event) => {
155 | if (hasSelection()) return;
156 | event.stopPropagation();
157 | this.setState({isEditingAfter: true});
158 | this.props.onEditBegin();
159 | };
160 |
161 | /**
162 | *
163 | */
164 | onPropertyClick = (event) => {
165 | if (hasSelection()) return;
166 | event.stopPropagation();
167 | this.setState({isEditingProperty: true});
168 | this.props.onEditBegin();
169 | };
170 |
171 | /**
172 | *
173 | */
174 | onValueClick = (event) => {
175 | if (hasSelection()) return;
176 | event.stopPropagation();
177 | this.setState({isEditingValue: true});
178 | this.props.onEditBegin();
179 | };
180 |
181 | /**
182 | *
183 | */
184 | onAreaBlur = (id, payload) => {
185 | this.setState({
186 | isEditingProperty: false,
187 | isEditingValue: false,
188 | isEditingAfter: false,
189 | });
190 | this.props.onEditEnd(id, payload);
191 | };
192 | }
193 |
194 | // =====================================================================================================================
195 | // D E F I N I T I O N
196 | // =====================================================================================================================
197 | export default Declaration;
198 |
--------------------------------------------------------------------------------
/lib/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports["default"] = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _cls = _interopRequireDefault(require("../utils/cls"));
13 |
14 | var _stylize = _interopRequireDefault(require("../utils/stylize"));
15 |
16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
17 |
18 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
19 |
20 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
21 |
22 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
23 |
24 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
25 |
26 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
27 |
28 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
29 |
30 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
31 |
32 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
33 |
34 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
35 |
36 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
37 |
38 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
39 |
40 | // =====================================================================================================================
41 | // D E C L A R A T I O N S
42 | // =====================================================================================================================
43 | var classes = (0, _stylize["default"])('Checkbox', {
44 | root: {
45 | position: 'relative',
46 | display: 'inline-block',
47 | verticalAlign: 'middle',
48 | marginTop: -2,
49 | marginRight: 4,
50 | width: 12,
51 | height: 12,
52 | border: 'solid 1px #333333',
53 | userSelect: 'none'
54 | },
55 | checked: {
56 | '&:after': {
57 | position: 'absolute',
58 | content: '""',
59 | left: 3,
60 | top: 0,
61 | width: 3,
62 | height: 7,
63 | border: 'solid 1px #000',
64 | borderTop: 'none',
65 | borderLeft: 'none',
66 | transform: 'rotate(45deg)'
67 | }
68 | },
69 | mixed: {
70 | // currently unused
71 | '&:after': {
72 | position: 'absolute',
73 | content: '""',
74 | left: 2,
75 | top: 2,
76 | width: 6,
77 | height: 6,
78 | background: '#333'
79 | }
80 | }
81 | }); // =====================================================================================================================
82 | // C O M P O N E N T
83 | // =====================================================================================================================
84 |
85 | var Checkbox = /*#__PURE__*/function (_React$PureComponent) {
86 | _inherits(Checkbox, _React$PureComponent);
87 |
88 | var _super = _createSuper(Checkbox);
89 |
90 | function Checkbox() {
91 | var _this;
92 |
93 | _classCallCheck(this, Checkbox);
94 |
95 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
96 | args[_key] = arguments[_key];
97 | }
98 |
99 | _this = _super.call.apply(_super, [this].concat(args));
100 |
101 | _defineProperty(_assertThisInitialized(_this), "onClick", function (event) {
102 | event.stopPropagation();
103 | var _this$props = _this.props,
104 | onTick = _this$props.onTick,
105 | id = _this$props.id,
106 | tick = _this$props.tick;
107 | onTick(id, [true, false, true][tick]); // 0 => true, 1 => false, 2 => true
108 | });
109 |
110 | return _this;
111 | }
112 |
113 | _createClass(Checkbox, [{
114 | key: "render",
115 | value:
116 | /**
117 | *
118 | */
119 | function render() {
120 | var tick = this.props.tick;
121 | return /*#__PURE__*/_react["default"].createElement("div", {
122 | className: (0, _cls["default"])(classes.root, tick === 1 && classes.checked, tick === 2 && classes.mixed),
123 | onClick: this.onClick
124 | });
125 | }
126 | /**
127 | *
128 | */
129 |
130 | }]);
131 |
132 | return Checkbox;
133 | }(_react["default"].PureComponent); // =====================================================================================================================
134 | // D E F I N I T I O N
135 | // =====================================================================================================================
136 |
137 |
138 | Checkbox.defaultProps = {
139 | tick: 0
140 | };
141 | var _default = Checkbox;
142 | exports["default"] = _default;
--------------------------------------------------------------------------------
/src/components/Rule.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {AFTER, AFTER_BEGIN, ATRULE, BEFORE, COMMENT, DECLARATION, RULE} from '../utils/COMMON';
4 | import cls from '../utils/cls';
5 | import Comment from './Comment';
6 | import Declaration from './Declaration';
7 | import Checkbox from './Checkbox';
8 | import stylize from '../utils/stylize';
9 | import clean from '../utils/clean';
10 | import Area from './Area';
11 | import Alert from './Alert';
12 | import hasSelection from '../utils/hasSelection';
13 |
14 | // =====================================================================================================================
15 | // D E C L A R A T I O N S
16 | // =====================================================================================================================
17 | const classes = stylize('Rule', {
18 | root: {
19 | // background: 'lime',
20 | },
21 | header: {
22 | padding: '2px 0',
23 | },
24 | selector: {
25 | color: 'black',
26 | cursor: 'text',
27 | borderBottom: '1px dashed transparent',
28 | '&:hover': {
29 | borderBottomColor: 'currentColor',
30 | },
31 | },
32 | block: {
33 | marginLeft: 16,
34 | whiteSpace: 'nowrap',
35 | },
36 | blockIsTop: {
37 | marginLeft: 4,
38 | },
39 | footer: {
40 | marginBottom: 4,
41 | },
42 | isEmpty: {
43 | padding: '0 6px',
44 | background: '#eee',
45 | '&:hover': {
46 | background: '#ddd',
47 | },
48 | },
49 | isInvalid: {
50 | textDecoration: 'line-through',
51 | textDecorationColor: '#939395',
52 | },
53 | });
54 |
55 | // =====================================================================================================================
56 | // C O M P O N E N T
57 | // =====================================================================================================================
58 | class Rule extends React.PureComponent {
59 | state = {
60 | isEditingSelector: false,
61 | isEditingBefore: false,
62 | isEditingAfterBegin: false,
63 | isEditingAfter: false,
64 | };
65 |
66 | /**
67 | *
68 | */
69 | render() {
70 | const {
71 | id,
72 | selector,
73 | hasBraceBegin,
74 | hasBraceEnd,
75 | hasSemicolon,
76 | kids,
77 | isTop,
78 | onEditBegin,
79 | onEditChange,
80 | onEditEnd,
81 | onTick,
82 | isValid,
83 | } = this.props;
84 | const {isEditingSelector, isEditingBefore, isEditingAfterBegin, isEditingAfter} = this.state;
85 |
86 | if (!kids.length && !selector.trim() && !hasBraceBegin && !hasBraceEnd && !hasSemicolon) {
87 | return null;
88 | }
89 |
90 | const cleanSelector = clean(selector);
91 |
92 | return (
93 |
94 | {isEditingBefore && this.renderArea(BEFORE)}
95 |
96 | {!isTop && (
97 |
98 |
99 |
100 | {isEditingSelector ? (
101 |
108 | ) : (
109 |
117 | {cleanSelector}
118 |
119 | )}
120 | {!isValid &&
}
121 | {!hasSemicolon &&
{' {'} }
122 |
123 | )}
124 |
125 |
126 | {isEditingAfterBegin && this.renderArea(AFTER_BEGIN)}
127 |
128 | {kids.map((item) => {
129 | const Component = typeToComponent[item.type];
130 | return (
131 |
139 | );
140 | })}
141 |
142 |
143 | {!isTop && !hasSemicolon && (
144 |
145 | {'}'}
146 |
147 | )}
148 |
149 | {isEditingAfter && this.renderArea(AFTER)}
150 |
151 | );
152 | }
153 |
154 | /**
155 | *
156 | */
157 | renderArea = (payloadProperty) => {
158 | const {id, onEditChange} = this.props;
159 | return (
160 |
170 | );
171 | };
172 |
173 | /**
174 | *
175 | */
176 | onSelectorClick = (event) => {
177 | if (hasSelection()) return;
178 | event.stopPropagation();
179 | this.setState({isEditingSelector: true});
180 | this.props.onEditBegin();
181 | };
182 |
183 | /**
184 | *
185 | */
186 | onBraceClick = (event) => {
187 | if (hasSelection()) return;
188 | event.stopPropagation();
189 | this.setState({isEditingBefore: true});
190 | this.props.onEditBegin();
191 | };
192 |
193 | /**
194 | *
195 | */
196 | onHeaderClick = (event) => {
197 | if (hasSelection()) return;
198 | event.stopPropagation();
199 | if (this.props.hasBraceBegin) {
200 | this.setState({isEditingAfterBegin: true});
201 | } else {
202 | this.setState({isEditingAfter: true});
203 | }
204 | this.props.onEditBegin();
205 | };
206 |
207 | /**
208 | *
209 | */
210 | onFooterClick = (event) => {
211 | if (hasSelection()) return;
212 | event.stopPropagation();
213 | this.setState({isEditingAfter: true});
214 | this.props.onEditBegin();
215 | };
216 |
217 | /**
218 | *
219 | */
220 | onAreaBlur = (id, payload) => {
221 | this.setState({
222 | isEditingSelector: false,
223 | isEditingBefore: false,
224 | isEditingAfterBegin: false,
225 | isEditingAfter: false,
226 | });
227 | this.props.onEditEnd(id, payload);
228 | };
229 | }
230 |
231 | // =====================================================================================================================
232 | // D E F I N I T I O N
233 | // =====================================================================================================================
234 | const typeToComponent = {
235 | [ATRULE]: Rule,
236 | [RULE]: Rule,
237 | [DECLARATION]: Declaration,
238 | [COMMENT]: Comment,
239 | };
240 | export default Rule;
241 |
--------------------------------------------------------------------------------
/tests/validate.test.js:
--------------------------------------------------------------------------------
1 | import parse from '../src/utils/parse.js';
2 | import validate from '../src/utils/validate.js';
3 |
4 | import {ATRULE, COMMENT, DECLARATION, RULE} from '../src/utils/COMMON.js';
5 |
6 | const tests = [
7 | // =================================================================================================================
8 | // R U L E S E T
9 | // =================================================================================================================
10 | // -----------------------------------------------------------------------------------------------------------------
11 | [1, ``, []],
12 | // -----------------------------------------------------------------------------------------------------------------
13 | [
14 | 1,
15 | ` `,
16 | [
17 | {
18 | type: RULE,
19 | selector: ` `,
20 | hasBraceBegin: false,
21 | hasBraceEnd: false,
22 | children: [],
23 | isValid: false,
24 | },
25 | ],
26 | ],
27 | // -----------------------------------------------------------------------------------------------------------------
28 | [
29 | 1,
30 | ` {background:red}`,
31 | [
32 | {
33 | type: RULE,
34 | selector: ` `,
35 | hasBraceBegin: true,
36 | hasBraceEnd: true,
37 | children: [
38 | {
39 | type: DECLARATION,
40 | hasSemicolon: false,
41 | property: `background`,
42 | hasColon: true,
43 | value: `red`,
44 | isValid: false, // because all children of invalid rules are also invalid
45 | },
46 | ],
47 | isValid: false,
48 | },
49 | ],
50 | ],
51 | // -----------------------------------------------------------------------------------------------------------------
52 | [
53 | 1,
54 | `div{background-color:red;background:blue}`,
55 | [
56 | {
57 | type: RULE,
58 | selector: `div`,
59 | hasBraceBegin: true,
60 | hasBraceEnd: true,
61 | isValid: true,
62 | children: [
63 | {
64 | type: DECLARATION,
65 | hasSemicolon: true,
66 | property: `background-color`,
67 | hasColon: true,
68 | value: `red`,
69 | isValid: false,
70 | },
71 | {
72 | type: DECLARATION,
73 | hasSemicolon: false,
74 | property: `background`,
75 | hasColon: true,
76 | value: `blue`,
77 | isValid: true,
78 | },
79 | ],
80 | },
81 | ],
82 | ],
83 | // -----------------------------------------------------------------------------------------------------------------
84 | [
85 | 1,
86 | `div{background:blue;background:blue}`,
87 | [
88 | {
89 | type: RULE,
90 | selector: `div`,
91 | hasBraceBegin: true,
92 | hasBraceEnd: true,
93 | isValid: true,
94 | children: [
95 | {
96 | type: DECLARATION,
97 | hasSemicolon: true,
98 | property: `background`,
99 | hasColon: true,
100 | value: `blue`,
101 | isValid: false,
102 | },
103 | {
104 | type: DECLARATION,
105 | hasSemicolon: false,
106 | property: `background`,
107 | hasColon: true,
108 | value: `blue`,
109 | isValid: true,
110 | },
111 | ],
112 | },
113 | ],
114 | ],
115 | // -----------------------------------------------------------------------------------------------------------------
116 | [
117 | 1,
118 | `div{background:blue}div{background:red}`,
119 | [
120 | {
121 | type: RULE,
122 | selector: `div`,
123 | hasBraceBegin: true,
124 | hasBraceEnd: true,
125 | isValid: true,
126 | children: [
127 | {
128 | type: DECLARATION,
129 | hasSemicolon: false,
130 | property: `background`,
131 | hasColon: true,
132 | value: `blue`,
133 | isValid: true,
134 | },
135 | ],
136 | },
137 | {
138 | type: RULE,
139 | selector: `div`,
140 | hasBraceBegin: true,
141 | hasBraceEnd: true,
142 | isValid: true,
143 | children: [
144 | {
145 | type: DECLARATION,
146 | hasSemicolon: false,
147 | property: `background`,
148 | hasColon: true,
149 | value: `red`,
150 | isValid: true,
151 | },
152 | ],
153 | },
154 | ],
155 | ],
156 | // -----------------------------------------------------------------------------------------------------------------
157 | [
158 | 1,
159 | ` @charset "utf-8" ;`,
160 | [
161 | {
162 | type: ATRULE,
163 | selector: ` @charset "utf-8" `,
164 | hasSemicolon: true,
165 | hasBraceBegin: false,
166 | hasBraceEnd: false,
167 | children: [],
168 | isValid: false, // will always be false due to severe restrictions
169 | },
170 | ],
171 | ],
172 | // -----------------------------------------------------------------------------------------------------------------
173 | [
174 | 1,
175 | `@viewport{ }`,
176 | [
177 | {
178 | type: RULE,
179 | selector: `@viewport`,
180 | hasBraceBegin: true,
181 | hasBraceEnd: true,
182 | isValid: false, // because support is conditioned by a flag
183 | children: [
184 | {
185 | type: DECLARATION,
186 | hasSemicolon: false,
187 | property: ` `,
188 | hasColon: false,
189 | value: ``,
190 | isValid: false,
191 | },
192 | ],
193 | },
194 | ],
195 | ],
196 | // -----------------------------------------------------------------------------------------------------------------
197 | [
198 | 1,
199 | `*.foo{}`,
200 | [
201 | {
202 | type: RULE,
203 | selector: `*.foo`, // Note: the browser removes the star in the stylesheet
204 | hasBraceBegin: true,
205 | hasBraceEnd: true,
206 | isValid: true,
207 | children: [],
208 | },
209 | ],
210 | ],
211 | ];
212 |
213 | const normalTests = [];
214 | const importantTests = [];
215 | tests.forEach((item) => {
216 | if (item[0] === 1) {
217 | normalTests.push(item);
218 | } else if (item[0] === 2) {
219 | importantTests.push(item);
220 | }
221 | });
222 |
223 | const usedTests = importantTests.length ? importantTests : normalTests;
224 | usedTests.forEach((item) => it(item[1], () => expect(validate(parse(item[1]))).toEqual(item[2])));
225 |
--------------------------------------------------------------------------------
/lib/components/Comment.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports["default"] = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _stylize = _interopRequireDefault(require("../utils/stylize"));
13 |
14 | var _clean = _interopRequireDefault(require("../utils/clean"));
15 |
16 | var _Checkbox = _interopRequireDefault(require("./Checkbox"));
17 |
18 | var _Area = _interopRequireDefault(require("./Area"));
19 |
20 | var _COMMON = require("../utils/COMMON");
21 |
22 | var _shorten = _interopRequireDefault(require("../utils/shorten"));
23 |
24 | var _hasSelection = _interopRequireDefault(require("../utils/hasSelection"));
25 |
26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
27 |
28 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
29 |
30 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
31 |
32 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
33 |
34 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
35 |
36 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
37 |
38 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
39 |
40 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
41 |
42 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
43 |
44 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
45 |
46 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
47 |
48 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
49 |
50 | // =====================================================================================================================
51 | // D E C L A R A T I O N S
52 | // =====================================================================================================================
53 | var classes = (0, _stylize["default"])('Comment', {
54 | root: {
55 | color: 'silver',
56 | padding: '2px 0',
57 | whiteSpace: 'nowrap',
58 | overflow: 'hidden',
59 | textOverflow: 'ellipsis'
60 | },
61 | content: {
62 | cursor: 'text',
63 | borderBottom: '1px dashed transparent',
64 | '&:hover': {
65 | borderBottomColor: 'currentColor'
66 | }
67 | },
68 | after: {
69 | marginTop: 4
70 | }
71 | });
72 | var MAX_CHARS_VALUE = 32; // how many characters to display in the value. Protects against giant base64.
73 |
74 | var MAX_CHARS_TITLE = 512; // how many characters to display in the tooltip. Protects against giant base64.
75 | // =====================================================================================================================
76 | // C O M P O N E N T
77 | // =====================================================================================================================
78 |
79 | var Comment = /*#__PURE__*/function (_React$PureComponent) {
80 | _inherits(Comment, _React$PureComponent);
81 |
82 | var _super = _createSuper(Comment);
83 |
84 | function Comment() {
85 | var _this;
86 |
87 | _classCallCheck(this, Comment);
88 |
89 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
90 | args[_key] = arguments[_key];
91 | }
92 |
93 | _this = _super.call.apply(_super, [this].concat(args));
94 |
95 | _defineProperty(_assertThisInitialized(_this), "state", {
96 | isEditingContent: false,
97 | isEditingAfter: false
98 | });
99 |
100 | _defineProperty(_assertThisInitialized(_this), "renderArea", function (payloadProperty, defaultValue) {
101 | var _this$props = _this.props,
102 | id = _this$props.id,
103 | onEditChange = _this$props.onEditChange;
104 | return /*#__PURE__*/_react["default"].createElement(_Area["default"], {
105 | defaultValue: defaultValue.trim(),
106 | id: id,
107 | payloadProperty: payloadProperty,
108 | onChange: onEditChange,
109 | onBlur: _this.onAreaBlur
110 | });
111 | });
112 |
113 | _defineProperty(_assertThisInitialized(_this), "onContentClick", function (event) {
114 | if ((0, _hasSelection["default"])()) return;
115 | event.stopPropagation();
116 |
117 | _this.setState({
118 | isEditingContent: true
119 | });
120 |
121 | _this.props.onEditBegin();
122 | });
123 |
124 | _defineProperty(_assertThisInitialized(_this), "onCommentClick", function (event) {
125 | if ((0, _hasSelection["default"])()) return;
126 | event.stopPropagation();
127 |
128 | _this.setState({
129 | isEditingAfter: true
130 | });
131 |
132 | _this.props.onEditBegin();
133 | });
134 |
135 | _defineProperty(_assertThisInitialized(_this), "onAreaBlur", function (id, payload) {
136 | _this.setState({
137 | isEditingContent: false,
138 | isEditingAfter: false
139 | });
140 |
141 | _this.props.onEditEnd(id, payload);
142 | });
143 |
144 | return _this;
145 | }
146 |
147 | _createClass(Comment, [{
148 | key: "render",
149 | value:
150 | /**
151 | *
152 | */
153 | function render() {
154 | var _this$props2 = this.props,
155 | id = _this$props2.id,
156 | content = _this$props2.content,
157 | onTick = _this$props2.onTick;
158 | var _this$state = this.state,
159 | isEditingContent = _this$state.isEditingContent,
160 | isEditingAfter = _this$state.isEditingAfter;
161 | var cleanContent = (0, _clean["default"])(content);
162 | var shortContent = cleanContent;
163 | var shortTitle = '';
164 |
165 | if (cleanContent.length > MAX_CHARS_VALUE) {
166 | shortContent = (0, _shorten["default"])(cleanContent, MAX_CHARS_VALUE);
167 | shortTitle = (0, _shorten["default"])(cleanContent, MAX_CHARS_TITLE);
168 | }
169 |
170 | return /*#__PURE__*/_react["default"].createElement("div", {
171 | className: classes.root,
172 | onClick: this.onCommentClick
173 | }, /*#__PURE__*/_react["default"].createElement(_Checkbox["default"], {
174 | id: id,
175 | tick: 0,
176 | onTick: onTick
177 | }), isEditingContent ? this.renderArea('content', content) : /*#__PURE__*/_react["default"].createElement("span", {
178 | className: classes.content,
179 | title: shortTitle,
180 | onClick: this.onContentClick
181 | }, '/*' + shortContent + '*/'), isEditingAfter && /*#__PURE__*/_react["default"].createElement("div", {
182 | className: classes.after
183 | }, /*#__PURE__*/_react["default"].createElement(_Checkbox["default"], {
184 | tick: 1
185 | }), this.renderArea(_COMMON.AFTER, '')));
186 | }
187 | /**
188 | *
189 | */
190 |
191 | }]);
192 |
193 | return Comment;
194 | }(_react["default"].PureComponent); // =====================================================================================================================
195 | // D E F I N I T I O N
196 | // =====================================================================================================================
197 |
198 |
199 | var _default = Comment;
200 | exports["default"] = _default;
--------------------------------------------------------------------------------
/lib/components/Area.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports["default"] = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _stylize = _interopRequireDefault(require("../utils/stylize"));
13 |
14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
15 |
16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
17 |
18 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
19 |
20 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
21 |
22 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
23 |
24 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
25 |
26 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
27 |
28 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
29 |
30 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
31 |
32 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
33 |
34 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
35 |
36 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
37 |
38 | // =====================================================================================================================
39 | // D E C L A R A T I O N S
40 | // =====================================================================================================================
41 | var classes = (0, _stylize["default"])('Area', {
42 | root: {
43 | fontFamily: 'Consolas, Liberation Mono, Menlo, monospace',
44 | // synced with StyleEditor's fontFamily
45 | fontSize: '12px',
46 | // synced with StyleEditor's fontSize
47 | resize: 'none',
48 | verticalAlign: 'middle',
49 | overflow: 'hidden',
50 | padding: 2,
51 | border: 'none',
52 | outline: 'solid 1px #ccc',
53 | outlineOffset: '-1px',
54 | minWidth: 4,
55 | textDecoration: 'none',
56 | // to combat `isInvalid` from upstream
57 | pointerEvents: 'auto !important' // to combat the general lock imposed by StyleEditor
58 |
59 | }
60 | }); // =====================================================================================================================
61 | // C O M P O N E N T
62 | // =====================================================================================================================
63 |
64 | var Area = /*#__PURE__*/function (_React$PureComponent) {
65 | _inherits(Area, _React$PureComponent);
66 |
67 | var _super = _createSuper(Area);
68 |
69 | function Area() {
70 | var _this;
71 |
72 | _classCallCheck(this, Area);
73 |
74 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
75 | args[_key] = arguments[_key];
76 | }
77 |
78 | _this = _super.call.apply(_super, [this].concat(args));
79 |
80 | _defineProperty(_assertThisInitialized(_this), "ref", /*#__PURE__*/_react["default"].createRef());
81 |
82 | _defineProperty(_assertThisInitialized(_this), "autoSize", function () {
83 | var textarea = _this.ref.current;
84 | textarea.style.whiteSpace = 'noWrap';
85 | textarea.style.width = '0';
86 | textarea.style.height = '0';
87 | var w = textarea.scrollWidth;
88 | var previousElement = textarea.previousElementSibling;
89 | var offset = 0;
90 |
91 | if (previousElement) {
92 | offset = previousElement.offsetLeft + previousElement.offsetWidth;
93 | }
94 |
95 | if (offset + w > textarea.parentNode.offsetWidth) {
96 | textarea.style.whiteSpace = 'normal';
97 | textarea.style.display = 'block';
98 | textarea.style.width = '100%';
99 | } else {
100 | textarea.style.display = 'inline-block';
101 | textarea.style.width = textarea.scrollWidth + 2 + 'px';
102 | }
103 |
104 | textarea.style.height = textarea.scrollHeight + 'px';
105 | });
106 |
107 | _defineProperty(_assertThisInitialized(_this), "onClick", function (event) {
108 | event.stopPropagation(); // prevent upstream handlers that would cause a blur
109 | });
110 |
111 | _defineProperty(_assertThisInitialized(_this), "onChange", function (event) {
112 | event.stopPropagation(); // we're handling the change manually and this event collides with us upstream
113 |
114 | _this.autoSize();
115 |
116 | var _this$props = _this.props,
117 | onChange = _this$props.onChange,
118 | id = _this$props.id,
119 | payloadProperty = _this$props.payloadProperty;
120 | onChange(id, _defineProperty({}, payloadProperty, event.currentTarget.value));
121 | });
122 |
123 | _defineProperty(_assertThisInitialized(_this), "onBlur", function (event) {
124 | var _this$props2 = _this.props,
125 | onBlur = _this$props2.onBlur,
126 | id = _this$props2.id,
127 | payloadProperty = _this$props2.payloadProperty;
128 | onBlur(id, _defineProperty({}, payloadProperty, event.currentTarget.value));
129 | });
130 |
131 | _defineProperty(_assertThisInitialized(_this), "onKeyDown", function (event) {
132 | // console.log(event.key);
133 | switch (event.key) {
134 | case 'Enter':
135 | if (event.shiftKey) {
136 | return; // allow Shift+Enter
137 | }
138 |
139 | _this.onBlur(event);
140 |
141 | cancelEvent(event);
142 | return;
143 |
144 | case 'Escape':
145 | event.currentTarget.value = _this.props.defaultValue;
146 |
147 | _this.onChange(event);
148 |
149 | _this.onBlur(event);
150 |
151 | cancelEvent(event);
152 | break;
153 |
154 | default:
155 | break;
156 | // allow any other characters
157 | }
158 | });
159 |
160 | return _this;
161 | }
162 |
163 | _createClass(Area, [{
164 | key: "render",
165 | value:
166 | /**
167 | *
168 | */
169 | function render() {
170 | var defaultValue = this.props.defaultValue;
171 | return /*#__PURE__*/_react["default"].createElement("textarea", {
172 | className: classes.root,
173 | defaultValue: defaultValue,
174 | onClick: this.onClick,
175 | onChange: this.onChange,
176 | onBlur: this.onBlur,
177 | onKeyDown: this.onKeyDown,
178 | ref: this.ref
179 | });
180 | }
181 | }, {
182 | key: "componentDidMount",
183 | value: function componentDidMount() {
184 | var textarea = this.ref.current;
185 | textarea.focus();
186 | textarea.select();
187 | this.autoSize();
188 | }
189 | /**
190 | *
191 | */
192 |
193 | }]);
194 |
195 | return Area;
196 | }(_react["default"].PureComponent); // =====================================================================================================================
197 | // H E L P E R S
198 | // =====================================================================================================================
199 |
200 | /**
201 | *
202 | */
203 |
204 |
205 | var cancelEvent = function cancelEvent(event) {
206 | event.preventDefault();
207 | event.stopPropagation();
208 | }; // =====================================================================================================================
209 | // D E F I N I T I O N
210 | // =====================================================================================================================
211 |
212 |
213 | var _default = Area;
214 | exports["default"] = _default;
--------------------------------------------------------------------------------
/lib/components/Declaration.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports["default"] = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _cls = _interopRequireDefault(require("../utils/cls"));
13 |
14 | var _stylize = _interopRequireDefault(require("../utils/stylize"));
15 |
16 | var _clean = _interopRequireDefault(require("../utils/clean"));
17 |
18 | var _shorten = _interopRequireDefault(require("../utils/shorten"));
19 |
20 | var _Checkbox = _interopRequireDefault(require("./Checkbox"));
21 |
22 | var _Area = _interopRequireDefault(require("./Area"));
23 |
24 | var _COMMON = require("../utils/COMMON");
25 |
26 | var _Alert = _interopRequireDefault(require("./Alert"));
27 |
28 | var _hasSelection = _interopRequireDefault(require("../utils/hasSelection"));
29 |
30 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
31 |
32 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
33 |
34 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
35 |
36 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
37 |
38 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
39 |
40 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
41 |
42 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
43 |
44 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
45 |
46 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
47 |
48 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
49 |
50 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
51 |
52 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
53 |
54 | // =====================================================================================================================
55 | // D E C L A R A T I O N S
56 | // =====================================================================================================================
57 | var classes = (0, _stylize["default"])('Declaration', {
58 | root: {
59 | padding: '2px 0',
60 | whiteSpace: 'nowrap',
61 | overflow: 'hidden',
62 | textOverflow: 'ellipsis'
63 | },
64 | property: {
65 | color: 'rgb(0, 116, 232)',
66 | // Firefox
67 | cursor: 'text',
68 | borderBottom: '1px dashed transparent',
69 | '&:hover': {
70 | borderBottomColor: 'currentColor'
71 | }
72 | },
73 | value: {
74 | color: 'rgb(221, 0, 169)',
75 | // Firefox
76 | cursor: 'text',
77 | borderBottom: '1px dashed transparent',
78 | '&:hover': {
79 | borderBottomColor: 'currentColor'
80 | }
81 | },
82 | isEmpty: {
83 | padding: '0 6px',
84 | background: '#eee',
85 | '&:hover': {
86 | background: '#ddd'
87 | }
88 | },
89 | after: {
90 | marginTop: 6
91 | },
92 | isInvalid: {
93 | textDecoration: 'line-through',
94 | textDecorationColor: '#939395'
95 | }
96 | });
97 | var MAX_CHARS_VALUE = 32; // how many characters to display in the value. Protects against giant base64.
98 |
99 | var MAX_CHARS_TITLE = 512; // how many characters to display in the tooltip. Protects against giant base64.
100 | // =====================================================================================================================
101 | // C O M P O N E N T
102 | // =====================================================================================================================
103 |
104 | var Declaration = /*#__PURE__*/function (_React$PureComponent) {
105 | _inherits(Declaration, _React$PureComponent);
106 |
107 | var _super = _createSuper(Declaration);
108 |
109 | function Declaration() {
110 | var _this;
111 |
112 | _classCallCheck(this, Declaration);
113 |
114 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
115 | args[_key] = arguments[_key];
116 | }
117 |
118 | _this = _super.call.apply(_super, [this].concat(args));
119 |
120 | _defineProperty(_assertThisInitialized(_this), "state", {
121 | isEditingProperty: false,
122 | isEditingValue: false,
123 | isEditingAfter: false
124 | });
125 |
126 | _defineProperty(_assertThisInitialized(_this), "renderArea", function (payloadProperty, defaultValue) {
127 | var _this$props = _this.props,
128 | id = _this$props.id,
129 | onEditChange = _this$props.onEditChange;
130 | return /*#__PURE__*/_react["default"].createElement(_Area["default"], {
131 | defaultValue: defaultValue.trim(),
132 | id: id,
133 | payloadProperty: payloadProperty,
134 | onChange: onEditChange,
135 | onBlur: _this.onAreaBlur
136 | });
137 | });
138 |
139 | _defineProperty(_assertThisInitialized(_this), "onDeclarationClick", function (event) {
140 | if ((0, _hasSelection["default"])()) return;
141 | event.stopPropagation();
142 |
143 | _this.setState({
144 | isEditingAfter: true
145 | });
146 |
147 | _this.props.onEditBegin();
148 | });
149 |
150 | _defineProperty(_assertThisInitialized(_this), "onPropertyClick", function (event) {
151 | if ((0, _hasSelection["default"])()) return;
152 | event.stopPropagation();
153 |
154 | _this.setState({
155 | isEditingProperty: true
156 | });
157 |
158 | _this.props.onEditBegin();
159 | });
160 |
161 | _defineProperty(_assertThisInitialized(_this), "onValueClick", function (event) {
162 | if ((0, _hasSelection["default"])()) return;
163 | event.stopPropagation();
164 |
165 | _this.setState({
166 | isEditingValue: true
167 | });
168 |
169 | _this.props.onEditBegin();
170 | });
171 |
172 | _defineProperty(_assertThisInitialized(_this), "onAreaBlur", function (id, payload) {
173 | _this.setState({
174 | isEditingProperty: false,
175 | isEditingValue: false,
176 | isEditingAfter: false
177 | });
178 |
179 | _this.props.onEditEnd(id, payload);
180 | });
181 |
182 | return _this;
183 | }
184 |
185 | _createClass(Declaration, [{
186 | key: "render",
187 | value:
188 | /**
189 | *
190 | */
191 | function render() {
192 | var _this$props2 = this.props,
193 | id = _this$props2.id,
194 | property = _this$props2.property,
195 | value = _this$props2.value,
196 | hasColon = _this$props2.hasColon,
197 | onTick = _this$props2.onTick,
198 | isValid = _this$props2.isValid;
199 | var _this$state = this.state,
200 | isEditingProperty = _this$state.isEditingProperty,
201 | isEditingValue = _this$state.isEditingValue,
202 | isEditingAfter = _this$state.isEditingAfter;
203 |
204 | if (!hasColon && !property.trim()) {
205 | return null;
206 | }
207 |
208 | var cleanProperty = (0, _clean["default"])(property);
209 | var cleanValue = (0, _clean["default"])(value);
210 | var shortValue = cleanValue;
211 | var shortTitle = '';
212 |
213 | if (cleanValue.length > MAX_CHARS_VALUE) {
214 | shortValue = (0, _shorten["default"])(cleanValue, MAX_CHARS_VALUE);
215 | shortTitle = (0, _shorten["default"])(cleanValue, MAX_CHARS_TITLE);
216 | }
217 |
218 | return /*#__PURE__*/_react["default"].createElement("div", {
219 | className: (0, _cls["default"])(classes.root, !isValid && classes.isInvalid),
220 | onClick: this.onDeclarationClick
221 | }, /*#__PURE__*/_react["default"].createElement(_Checkbox["default"], {
222 | id: id,
223 | tick: 1,
224 | onTick: onTick
225 | }), isEditingProperty ? this.renderArea('property', property) : /*#__PURE__*/_react["default"].createElement("span", {
226 | className: (0, _cls["default"])(classes.property, !cleanProperty && classes.isEmpty),
227 | onClick: this.onPropertyClick
228 | }, cleanProperty), ': ', isEditingValue ? this.renderArea('value', value) : /*#__PURE__*/_react["default"].createElement("span", {
229 | className: (0, _cls["default"])(classes.value, !cleanValue && classes.isEmpty),
230 | onClick: this.onValueClick,
231 | title: shortTitle
232 | }, shortValue), ';', !isValid && /*#__PURE__*/_react["default"].createElement(_Alert["default"], null), isEditingAfter && /*#__PURE__*/_react["default"].createElement("div", {
233 | className: classes.after
234 | }, /*#__PURE__*/_react["default"].createElement(_Checkbox["default"], {
235 | tick: 1
236 | }), this.renderArea(_COMMON.AFTER, '')));
237 | }
238 | /**
239 | *
240 | */
241 |
242 | }]);
243 |
244 | return Declaration;
245 | }(_react["default"].PureComponent); // =====================================================================================================================
246 | // D E F I N I T I O N
247 | // =====================================================================================================================
248 |
249 |
250 | var _default = Declaration;
251 | exports["default"] = _default;
--------------------------------------------------------------------------------
/lib/components/Rule.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports["default"] = void 0;
9 |
10 | var _react = _interopRequireDefault(require("react"));
11 |
12 | var _COMMON = require("../utils/COMMON");
13 |
14 | var _cls = _interopRequireDefault(require("../utils/cls"));
15 |
16 | var _Comment = _interopRequireDefault(require("./Comment"));
17 |
18 | var _Declaration = _interopRequireDefault(require("./Declaration"));
19 |
20 | var _Checkbox = _interopRequireDefault(require("./Checkbox"));
21 |
22 | var _stylize = _interopRequireDefault(require("../utils/stylize"));
23 |
24 | var _clean = _interopRequireDefault(require("../utils/clean"));
25 |
26 | var _Area = _interopRequireDefault(require("./Area"));
27 |
28 | var _Alert = _interopRequireDefault(require("./Alert"));
29 |
30 | var _hasSelection = _interopRequireDefault(require("../utils/hasSelection"));
31 |
32 | var _typeToComponent;
33 |
34 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
35 |
36 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
37 |
38 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
39 |
40 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
41 |
42 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
43 |
44 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
45 |
46 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
47 |
48 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
49 |
50 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
51 |
52 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
53 |
54 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
55 |
56 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
57 |
58 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
59 |
60 | // =====================================================================================================================
61 | // D E C L A R A T I O N S
62 | // =====================================================================================================================
63 | var classes = (0, _stylize["default"])('Rule', {
64 | root: {// background: 'lime',
65 | },
66 | header: {
67 | padding: '2px 0'
68 | },
69 | selector: {
70 | color: 'black',
71 | cursor: 'text',
72 | borderBottom: '1px dashed transparent',
73 | '&:hover': {
74 | borderBottomColor: 'currentColor'
75 | }
76 | },
77 | block: {
78 | marginLeft: 16,
79 | whiteSpace: 'nowrap'
80 | },
81 | blockIsTop: {
82 | marginLeft: 4
83 | },
84 | footer: {
85 | marginBottom: 4
86 | },
87 | isEmpty: {
88 | padding: '0 6px',
89 | background: '#eee',
90 | '&:hover': {
91 | background: '#ddd'
92 | }
93 | },
94 | isInvalid: {
95 | textDecoration: 'line-through',
96 | textDecorationColor: '#939395'
97 | }
98 | }); // =====================================================================================================================
99 | // C O M P O N E N T
100 | // =====================================================================================================================
101 |
102 | var Rule = /*#__PURE__*/function (_React$PureComponent) {
103 | _inherits(Rule, _React$PureComponent);
104 |
105 | var _super = _createSuper(Rule);
106 |
107 | function Rule() {
108 | var _this;
109 |
110 | _classCallCheck(this, Rule);
111 |
112 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
113 | args[_key] = arguments[_key];
114 | }
115 |
116 | _this = _super.call.apply(_super, [this].concat(args));
117 |
118 | _defineProperty(_assertThisInitialized(_this), "state", {
119 | isEditingSelector: false,
120 | isEditingBefore: false,
121 | isEditingAfterBegin: false,
122 | isEditingAfter: false
123 | });
124 |
125 | _defineProperty(_assertThisInitialized(_this), "renderArea", function (payloadProperty) {
126 | var _this$props = _this.props,
127 | id = _this$props.id,
128 | onEditChange = _this$props.onEditChange;
129 | return /*#__PURE__*/_react["default"].createElement("div", null, /*#__PURE__*/_react["default"].createElement(_Checkbox["default"], {
130 | tick: 1
131 | }), /*#__PURE__*/_react["default"].createElement(_Area["default"], {
132 | defaultValue: '',
133 | id: id,
134 | payloadProperty: payloadProperty,
135 | onChange: onEditChange,
136 | onBlur: _this.onAreaBlur
137 | }));
138 | });
139 |
140 | _defineProperty(_assertThisInitialized(_this), "onSelectorClick", function (event) {
141 | if ((0, _hasSelection["default"])()) return;
142 | event.stopPropagation();
143 |
144 | _this.setState({
145 | isEditingSelector: true
146 | });
147 |
148 | _this.props.onEditBegin();
149 | });
150 |
151 | _defineProperty(_assertThisInitialized(_this), "onBraceClick", function (event) {
152 | if ((0, _hasSelection["default"])()) return;
153 | event.stopPropagation();
154 |
155 | _this.setState({
156 | isEditingBefore: true
157 | });
158 |
159 | _this.props.onEditBegin();
160 | });
161 |
162 | _defineProperty(_assertThisInitialized(_this), "onHeaderClick", function (event) {
163 | if ((0, _hasSelection["default"])()) return;
164 | event.stopPropagation();
165 |
166 | if (_this.props.hasBraceBegin) {
167 | _this.setState({
168 | isEditingAfterBegin: true
169 | });
170 | } else {
171 | _this.setState({
172 | isEditingAfter: true
173 | });
174 | }
175 |
176 | _this.props.onEditBegin();
177 | });
178 |
179 | _defineProperty(_assertThisInitialized(_this), "onFooterClick", function (event) {
180 | if ((0, _hasSelection["default"])()) return;
181 | event.stopPropagation();
182 |
183 | _this.setState({
184 | isEditingAfter: true
185 | });
186 |
187 | _this.props.onEditBegin();
188 | });
189 |
190 | _defineProperty(_assertThisInitialized(_this), "onAreaBlur", function (id, payload) {
191 | _this.setState({
192 | isEditingSelector: false,
193 | isEditingBefore: false,
194 | isEditingAfterBegin: false,
195 | isEditingAfter: false
196 | });
197 |
198 | _this.props.onEditEnd(id, payload);
199 | });
200 |
201 | return _this;
202 | }
203 |
204 | _createClass(Rule, [{
205 | key: "render",
206 | value:
207 | /**
208 | *
209 | */
210 | function render() {
211 | var _this$props2 = this.props,
212 | id = _this$props2.id,
213 | selector = _this$props2.selector,
214 | hasBraceBegin = _this$props2.hasBraceBegin,
215 | hasBraceEnd = _this$props2.hasBraceEnd,
216 | hasSemicolon = _this$props2.hasSemicolon,
217 | kids = _this$props2.kids,
218 | isTop = _this$props2.isTop,
219 | onEditBegin = _this$props2.onEditBegin,
220 | onEditChange = _this$props2.onEditChange,
221 | onEditEnd = _this$props2.onEditEnd,
222 | onTick = _this$props2.onTick,
223 | isValid = _this$props2.isValid;
224 | var _this$state = this.state,
225 | isEditingSelector = _this$state.isEditingSelector,
226 | isEditingBefore = _this$state.isEditingBefore,
227 | isEditingAfterBegin = _this$state.isEditingAfterBegin,
228 | isEditingAfter = _this$state.isEditingAfter;
229 |
230 | if (!kids.length && !selector.trim() && !hasBraceBegin && !hasBraceEnd && !hasSemicolon) {
231 | return null;
232 | }
233 |
234 | var cleanSelector = (0, _clean["default"])(selector);
235 | return /*#__PURE__*/_react["default"].createElement("div", {
236 | className: classes.root
237 | }, isEditingBefore && this.renderArea(_COMMON.BEFORE), !isTop && /*#__PURE__*/_react["default"].createElement("div", {
238 | className: classes.header,
239 | onClick: this.onHeaderClick
240 | }, /*#__PURE__*/_react["default"].createElement(_Checkbox["default"], {
241 | id: id,
242 | tick: 1,
243 | onTick: onTick
244 | }), isEditingSelector ? /*#__PURE__*/_react["default"].createElement(_Area["default"], {
245 | defaultValue: selector.trim(),
246 | id: id,
247 | payloadProperty: 'selector',
248 | onChange: onEditChange,
249 | onBlur: this.onAreaBlur
250 | }) : /*#__PURE__*/_react["default"].createElement("span", {
251 | className: (0, _cls["default"])(classes.selector, !cleanSelector && classes.isEmpty, !isValid && classes.isInvalid),
252 | onClick: this.onSelectorClick
253 | }, cleanSelector), !isValid && /*#__PURE__*/_react["default"].createElement(_Alert["default"], null), !hasSemicolon && /*#__PURE__*/_react["default"].createElement("span", {
254 | onClick: this.onBraceClick
255 | }, ' {')), /*#__PURE__*/_react["default"].createElement("div", {
256 | className: (0, _cls["default"])(classes.block, isTop && classes.blockIsTop)
257 | }, isEditingAfterBegin && this.renderArea(_COMMON.AFTER_BEGIN), kids.map(function (item) {
258 | var Component = typeToComponent[item.type];
259 | return /*#__PURE__*/_react["default"].createElement(Component, _extends({}, item, {
260 | key: item.id,
261 | onEditBegin: onEditBegin,
262 | onEditChange: onEditChange,
263 | onEditEnd: onEditEnd,
264 | onTick: onTick
265 | }));
266 | })), !isTop && !hasSemicolon && /*#__PURE__*/_react["default"].createElement("div", {
267 | className: classes.footer,
268 | onClick: this.onFooterClick
269 | }, '}'), isEditingAfter && this.renderArea(_COMMON.AFTER));
270 | }
271 | /**
272 | *
273 | */
274 |
275 | }]);
276 |
277 | return Rule;
278 | }(_react["default"].PureComponent); // =====================================================================================================================
279 | // D E F I N I T I O N
280 | // =====================================================================================================================
281 |
282 |
283 | var typeToComponent = (_typeToComponent = {}, _defineProperty(_typeToComponent, _COMMON.ATRULE, Rule), _defineProperty(_typeToComponent, _COMMON.RULE, Rule), _defineProperty(_typeToComponent, _COMMON.DECLARATION, _Declaration["default"]), _defineProperty(_typeToComponent, _COMMON.COMMENT, _Comment["default"]), _typeToComponent);
284 | var _default = Rule;
285 | exports["default"] = _default;
--------------------------------------------------------------------------------
/website/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import StyleEditor from 'react-style-editor';
3 | // import StyleEditor from './tmp/components/StyleEditor';
4 | import Typography from '@material-ui/core/Typography';
5 | import Button from '@material-ui/core/Button';
6 | import {withStyles} from '@material-ui/styles';
7 | import clsx from 'clsx';
8 |
9 | import LARGE_SAMPLE from './LARGE_SAMPLE';
10 |
11 | // =====================================================================================================================
12 | // D E C L A R A T I O N S
13 | // =====================================================================================================================
14 | const styles = {
15 | '@global': {
16 | html: {
17 | fontFamily: 'Roboto, sans-serif',
18 | fontVariantLigatures: 'none',
19 | },
20 | body: {
21 | margin: 0,
22 | },
23 | code: {
24 | fontFamily: 'Consolas, Liberation Mono, Menlo, monospace', // GitHub
25 | backgroundColor: 'rgba(27,31,35,.05)',
26 | borderRadius: 3,
27 | fontSize: '85%',
28 | margin: 0,
29 | padding: '.2em .4em',
30 | },
31 | },
32 | root: {
33 | paddingBottom: 64,
34 | },
35 | container: {
36 | paddingTop: 32,
37 | padding: '32px 8px 0 8px',
38 | maxWidth: 950,
39 | margin: 'auto',
40 | },
41 | masthead: {
42 | position: 'relative',
43 | },
44 | main: {
45 | background: '#b3e5fc',
46 | marginTop: 32,
47 | textAlign: 'center',
48 | paddingBottom: 32,
49 | },
50 | styleEditor: {
51 | background: 'white',
52 | width: 320,
53 | height: 240,
54 | maxWidth: '100%',
55 | overflow: 'scroll',
56 | resize: 'both',
57 | margin: '8px auto 16px auto',
58 | },
59 | isLarge: {
60 | width: '100%',
61 | },
62 | stars: {
63 | float: 'right',
64 | width: 160,
65 | height: 30,
66 | border: 'none',
67 | overflow: 'hidden',
68 | },
69 | comparisonStyleEditor: {
70 | height: 240,
71 | border: 'none !important',
72 | },
73 | comparisonTextarea: {
74 | width: '100%',
75 | height: 240,
76 | whiteSpace: 'pre',
77 | wordBreak: 'break-all',
78 | border: 'none',
79 | resize: 'none',
80 | '&:focus-visible': {
81 | outlineWidth: 0,
82 | },
83 | },
84 | table: {
85 | marginTop: 16,
86 | width: '100%',
87 | borderCollapse: 'collapse',
88 | tableLayout: 'fixed',
89 | '& th': {
90 | fontWeight: 500,
91 | textAlign: 'left',
92 | },
93 | '& th, & td': {
94 | width: '50%',
95 | padding: 8,
96 | borderLeft: 'solid 1px silver',
97 | '&:first-child': {
98 | border: 'none',
99 | },
100 | },
101 | },
102 | exampleSurgery: {
103 | marginTop: 32,
104 | },
105 | surgeryTextarea: {
106 | width: '100%',
107 | height: 240,
108 | whiteSpace: 'pre-wrap',
109 | wordBreak: 'break-all',
110 | border: 'none',
111 | resize: 'none',
112 | },
113 | surgeryStyleEditor: {
114 | height: 240,
115 | border: 'none !important',
116 | },
117 | };
118 | const SMALL_SAMPLE = `
119 | div{color:red;foo}
120 | /* Hello, World! */
121 | @media screen {
122 | article {
123 | display: flex;
124 | }
125 | }
126 | `;
127 | const COMPARISON_SAMPLE = 'div{color:red;foo';
128 | const SURGERY_SAMPLE =
129 | 'html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;' +
130 | '-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}' +
131 | 'h1{font-size:2em;margin:.67em 0}figcaption,figure{display:block}figure{margin:1em 40px}' +
132 | 'hr{box-sizing:content-box;height:0;overflow:visible}';
133 |
134 | // =====================================================================================================================
135 | // C O M P O N E N T
136 | // =====================================================================================================================
137 | class App extends React.PureComponent {
138 | state = {
139 | isLarge: false,
140 | main: SMALL_SAMPLE,
141 | mainCount: 0,
142 | comparisonPreserved: '',
143 | comparisonMachine: '',
144 | comparisonPretty: '',
145 | surgery: SURGERY_SAMPLE,
146 | };
147 | comparisonRef = React.createRef();
148 |
149 | render() {
150 | const {classes} = this.props;
151 | const {isLarge, main, mainCount, comparisonPreserved, comparisonMachine, comparisonPretty, surgery} =
152 | this.state;
153 | return (
154 |
155 |
156 |
164 |
165 |
166 | React Style Editor
167 |
168 |
169 |
170 | A React component that displays and edits CSS, similar to the browser's DevTools.
171 |
172 |
173 |
174 |
175 |
176 | You can type, copy or paste CSS here:
177 |
182 |
183 | Clean
184 | {' '}
185 |
186 | Use a large sample
187 |
188 |
189 |
190 |
191 |
192 |
193 | Features
194 |
195 |
196 | Parses any CSS string and formats it in a familiar fashion
197 | Validates each rule and each declaration using the browsers's own engine
198 | Facilitates commenting the CSS code through checkbox toggling
199 | Allows easy additions by clicking next to the desired location
200 | Has no dependencies (other than React)
201 | Is tiny (< 10 KB minified)
202 | Is customizable through classes
203 | Offers 3 output formats:
204 |
205 | the code with preserved formatting
206 | a machine-friendly model of the code (recursive array of objects)
207 | the prettified code
208 |
209 |
210 |
211 |
212 |
213 |
214 | Example: Output formats
215 |
216 | Change the code in the StyleEditor to compare the different outputs received by
217 |
onChange.
218 |
219 | The original input was
{COMPARISON_SAMPLE}.
220 |
221 |
222 |
223 | StyleEditor
224 |
225 | Format preserved
226 |
227 |
228 | Format machine
229 |
230 |
231 | Format pretty
232 |
233 |
234 |
235 |
236 |
237 |
238 |
245 |
246 |
247 |
252 |
253 |
254 |
259 |
260 |
261 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 | Example: Surgery of minified code
275 |
276 | Change anything in either of the two sides (they are synchronized):
277 |
278 |
279 |
280 | Minified code
281 | StyleEditor
282 |
283 |
284 |
285 |
286 |
287 |
292 |
293 |
294 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | Documentation
309 |
310 | The API is described in the
repository .
311 |
312 |
313 | );
314 | }
315 |
316 | componentDidMount() {
317 | // The following is a hack that accesses private a private function inside StyleEditor. TODO: Find another way.
318 | this.comparisonRef.current.announceOnChange(COMPARISON_SAMPLE);
319 | }
320 |
321 | /**
322 | *
323 | */
324 | onCleanClick = () => {
325 | this.setState({
326 | main: '',
327 | mainCount: this.state.mainCount + 1,
328 | });
329 | };
330 |
331 | /**
332 | *
333 | */
334 | onLargeClick = () => {
335 | this.setState({
336 | isLarge: true,
337 | main: LARGE_SAMPLE,
338 | mainCount: this.state.mainCount + 1,
339 | });
340 | };
341 |
342 | /**
343 | *
344 | */
345 | onComparisonStyleEditorChange = (list) => {
346 | this.setState({
347 | comparisonPreserved: list[0],
348 | comparisonMachine: JSON.stringify(list[1], null, 4),
349 | comparisonPretty: list[2],
350 | });
351 | };
352 |
353 | /**
354 | *
355 | */
356 | onSurgeryTextareaChange = (event) => {
357 | this.setState({
358 | surgery: event.currentTarget.value,
359 | });
360 | };
361 |
362 | /**
363 | *
364 | */
365 | onSurgeryStyleEditorChange = (payload) => {
366 | console.log('payload:', payload);
367 | this.setState({
368 | surgery: payload,
369 | });
370 | };
371 | }
372 |
373 | export default withStyles(styles)(App);
374 |
--------------------------------------------------------------------------------
/src/components/StyleEditor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Rule from './Rule';
4 | import Area from './Area';
5 | import {AFTER, AFTER_BEGIN, ATRULE, BEFORE, COMMENT, DECLARATION, RULE} from '../utils/COMMON';
6 | import stylize, {prepareStyling, releaseStyling} from '../utils/stylize';
7 | import analyze from '../utils/analyze';
8 | import modify from '../utils/modify';
9 | import stringify from '../utils/stringify';
10 | import prettify from '../utils/prettify';
11 | import ignore from '../utils/ignore';
12 | import unignore from '../utils/unignore';
13 | import cls from '../utils/cls';
14 | import hasSelection from '../utils/hasSelection';
15 |
16 | // =====================================================================================================================
17 | // D E C L A R A T I O N S
18 | // =====================================================================================================================
19 | const classes = stylize('StyleEditor', {
20 | root: {
21 | fontFamily: 'Consolas, Liberation Mono, Menlo, monospace', // GitHub
22 | fontSize: '12px', // Chrome
23 | textAlign: 'left',
24 | overflow: 'auto',
25 | color: 'black',
26 | position: 'relative',
27 | cursor: 'default',
28 | boxSizing: 'border-box',
29 | border: 'solid 1px silver',
30 | padding: 4,
31 | '& *': {
32 | boxSizing: 'border-box',
33 | },
34 | },
35 | isEmpty: {
36 | minHeight: 20,
37 | cursor: 'text',
38 | background: '#eee',
39 | '&:hover': {
40 | background: '#ddd',
41 | },
42 | },
43 | isLocked: {
44 | '& *': {
45 | pointerEvents: 'none',
46 | },
47 | },
48 | });
49 | let hasControlledWarning = false;
50 |
51 | // =====================================================================================================================
52 | // C O M P O N E N T
53 | // =====================================================================================================================
54 | class StyleEditor extends React.Component {
55 | // Private variables:
56 | currentRules = [];
57 | memoRules = this.currentRules; // a simulation of `memoize-one`
58 | memoCSS = ''; // a simulation of `memoize-one`
59 | isControlled = false;
60 |
61 | /**
62 | *
63 | */
64 | constructor(props) {
65 | super(props);
66 | prepareStyling();
67 | this.state = {
68 | isEditing: false,
69 | hasArea: false,
70 | internalValue: props.defaultValue,
71 | };
72 | }
73 |
74 | /**
75 | *
76 | */
77 | render() {
78 | const {value, className, readOnly, ...other} = this.props;
79 | const {isEditing, hasArea, internalValue} = this.state;
80 | delete other.outputFormats; // not used in render
81 |
82 | this.isControlled = checkIsControlled(this.props);
83 | const usedValue = this.isControlled ? value : internalValue;
84 |
85 | this.currentRules = typeof usedValue === 'string' ? this.computeRules(usedValue) : usedValue;
86 | const isEmpty = !this.currentRules.length;
87 |
88 | return (
89 |
100 | {!isEmpty && (
101 |
110 | )}
111 | {hasArea && (
112 |
119 | )}
120 |
121 | );
122 | }
123 |
124 | /**
125 | *
126 | */
127 | // componentDidMount() {
128 | // this.announceOnChange(this.currentRules);
129 | // }
130 |
131 | /**
132 | * Under no circumstances do we allow updates while an edit is on-going.
133 | * Alas, because of this small restriction, we had to quit using PureComponent and had to duplicate its
134 | * functionality by manually checking if values have actually changed.
135 | */
136 | shouldComponentUpdate(nextProps, nextState, nextContext) {
137 | if (this.state.isEditing) {
138 | return nextState.isEditing === false; // allow updates only in order to exit editing mode
139 | }
140 | for (const key in nextProps) {
141 | if (this.props[key] !== nextProps[key]) {
142 | if (key !== 'defaultValue') {
143 | // we're ignoring changes to defaultValue
144 | return true;
145 | }
146 | }
147 | }
148 | for (const key in nextState) {
149 | if (this.state[key] !== nextState[key]) {
150 | return true;
151 | }
152 | }
153 | return false;
154 | }
155 |
156 | /**
157 | *
158 | */
159 | componentWillUnmount() {
160 | releaseStyling();
161 | }
162 |
163 | /**
164 | *
165 | */
166 | computeRules = (css) => {
167 | if (this.memoCSS === css) {
168 | return this.memoRules;
169 | }
170 | const rules = analyze(css);
171 | this.memoCSS = css;
172 | this.memoRules = rules;
173 | return rules;
174 | };
175 |
176 | /**
177 | *
178 | */
179 | onEditBegin = () => {
180 | this.setState({
181 | isEditing: true,
182 | });
183 | };
184 |
185 | /**
186 | *
187 | */
188 | onEditChange = (id, payload) => {
189 | const {onChange} = this.props;
190 | if (onChange) {
191 | const freshBlob = computeBlobFromPayload(this.currentRules, id, payload);
192 | this.announceOnChange(freshBlob);
193 | }
194 | };
195 |
196 | /**
197 | *
198 | */
199 | announceOnChange = (rulesOrBlob) => {
200 | const {onChange, outputFormats} = this.props;
201 | if (onChange) {
202 | let rules = typeof rulesOrBlob === 'string' ? null : rulesOrBlob; // null means lazy initialization
203 | const formats = outputFormats.replace(/\s/g, '').split(',');
204 | const output = [];
205 | for (const format of formats) {
206 | switch (format) {
207 | case 'preserved':
208 | if (rules) {
209 | output.push(stringify(rulesOrBlob));
210 | } else {
211 | output.push(rulesOrBlob);
212 | }
213 | break;
214 | case 'machine':
215 | if (!rules) {
216 | rules = this.computeRules(rulesOrBlob);
217 | }
218 | output.push(JSON.parse(JSON.stringify(rules))); // TODO: use something faster
219 | break;
220 | case 'pretty':
221 | default:
222 | if (!rules) {
223 | rules = this.computeRules(rulesOrBlob);
224 | }
225 | output.push(prettify(rules));
226 | break;
227 | }
228 | }
229 | onChange(output.length > 1 ? output : output[0] || '');
230 | }
231 | };
232 |
233 | /**
234 | *
235 | */
236 | onEditEnd = (id, payload) => {
237 | if (this.isControlled) {
238 | this.setState({
239 | isEditing: false,
240 | });
241 | // there's no need to do anything else. Our parent already has the payload from the onChange event
242 | } else {
243 | // uncontrolled
244 | this.setState({
245 | isEditing: false,
246 | internalValue: computeBlobFromPayload(this.currentRules, id, payload),
247 | });
248 | }
249 | };
250 |
251 | /**
252 | *
253 | */
254 | onTick = (id, desiredTick) => {
255 | const freshBlob = desiredTick ? unignore(this.currentRules, id) : ignore(this.currentRules, id);
256 | this.announceOnChange(freshBlob);
257 | if (!this.isControlled) {
258 | this.setState({
259 | internalValue: freshBlob,
260 | });
261 | }
262 | };
263 |
264 | /**
265 | *
266 | */
267 | onCopy = (event) => {
268 | if (hasSelection()) return;
269 | const blob = prettify(this.currentRules);
270 | event.nativeEvent.clipboardData.setData('text/plain', blob);
271 | event.preventDefault();
272 | };
273 |
274 | /**
275 | *
276 | */
277 | onClick = () => {
278 | if (hasSelection()) return;
279 | this.setState({
280 | isEditing: true,
281 | hasArea: true,
282 | });
283 | };
284 |
285 | /**
286 | *
287 | */
288 | onAreaChange = (id, payload) => {
289 | const {onChange} = this.props;
290 | if (onChange) {
291 | this.announceOnChange(payload.selector);
292 | }
293 | };
294 |
295 | /**
296 | *
297 | */
298 | onAreaBlur = (id, payload) => {
299 | if (this.isControlled) {
300 | this.setState({
301 | isEditing: false,
302 | hasArea: false,
303 | });
304 | // there's no need to do anything else. Our parent already has the payload from the onChange event
305 | } else {
306 | // uncontrolled
307 | this.setState({
308 | isEditing: false,
309 | hasArea: false,
310 | internalValue: payload.selector,
311 | });
312 | }
313 | };
314 | }
315 |
316 | // =====================================================================================================================
317 | // H E L P E R S
318 | // =====================================================================================================================
319 | /**
320 | *
321 | */
322 | const checkIsControlled = (props) => {
323 | if (props.value !== undefined) {
324 | if (!props.onChange && !props.readOnly && !hasControlledWarning) {
325 | hasControlledWarning = true;
326 | if (window.console && window.console.warn) {
327 | console.warn(
328 | 'You provided a `value` prop to StyleEditor without an `onChange` handler. ' +
329 | 'This will render a read-only field. If the StyleEditor should be mutable, use `defaultValue`. ' +
330 | 'Otherwise, set either `onChange` or `readOnly`.'
331 | );
332 | }
333 | }
334 | return true;
335 | } else {
336 | return false;
337 | }
338 | };
339 |
340 | /**
341 | *
342 | */
343 | const computeBlobFromPayload = (rules, id, payload) => {
344 | // Without deep-cloning, writing inside #foo{} produces: #foo{c;} #foo{co;c;} #foo{col;co;c;} etc.
345 | // TODO: find a better way
346 | const rulesDeepClone = JSON.parse(JSON.stringify(rules));
347 |
348 | const {freshRules, freshNode, parentNode} = modify(rulesDeepClone, id, payload);
349 | if (payload[AFTER_BEGIN]) {
350 | // can only be dispatched by AT/RULE
351 | const node = createTemporaryDeclaration(payload[AFTER_BEGIN]);
352 | freshNode.kids.unshift(node);
353 | } else if (payload[BEFORE]) {
354 | // can only be dispatched by AT/RULE and can only create AT/RULE
355 | const node = createTemporaryRule(payload[BEFORE]);
356 | const siblings = parentNode.kids;
357 | const index = siblings.findIndex((item) => item.id === id);
358 | siblings.splice(index, 0, node);
359 | } else if (payload[AFTER]) {
360 | // can be dispatched by any type of node
361 | let text = payload[AFTER];
362 | let node;
363 | switch (
364 | freshNode.type // freshNode is in fact the anchor node, NOT the node we're about to create
365 | ) {
366 | case ATRULE:
367 | if (freshNode.hasBraceBegin && !freshNode.hasBraceEnd) {
368 | text = '}' + text;
369 | } else if (!freshNode.hasSemicolon) {
370 | text = ';' + text;
371 | }
372 | node = createTemporaryRule(text);
373 | break;
374 | case RULE:
375 | if (!freshNode.hasBraceEnd) {
376 | text = '}' + text;
377 | }
378 | node = createTemporaryRule(text);
379 | break;
380 | case DECLARATION:
381 | if (!freshNode.hasSemicolon) {
382 | text = ';' + text;
383 | }
384 | node = createTemporaryDeclaration(text);
385 | break;
386 | case COMMENT:
387 | if (!freshNode.hasSlashEnd) {
388 | text = '*/' + text;
389 | }
390 | if (parentNode.type === ATRULE) {
391 | node = createTemporaryRule(text);
392 | } else {
393 | node = createTemporaryDeclaration(text);
394 | }
395 | break;
396 | default:
397 | // nothing
398 | }
399 | const siblings = parentNode.kids;
400 | const index = siblings.findIndex((item) => item.id === id);
401 | siblings.splice(index + 1, 0, node);
402 | } else if (payload.value) {
403 | freshNode.hasColon = true;
404 | }
405 | return stringify(freshRules);
406 | };
407 |
408 | /**
409 | *
410 | */
411 | const createTemporaryDeclaration = (text) => {
412 | if (!text.match(/;\s*$/)) {
413 | // doesn't end with semicolon
414 | text += ';'; // close it
415 | }
416 | return {
417 | type: DECLARATION,
418 | property: text,
419 | value: '',
420 | };
421 | };
422 |
423 | /**
424 | *
425 | */
426 | const createTemporaryRule = (text) => {
427 | if (text.match(/^\s*@/)) {
428 | // ATRULE
429 | if (!text.match(/[{};]/)) {
430 | // doesn't contain braces or semicolons
431 | text += ';'; // close it. We assume this is not a nested ATRULE
432 | }
433 | } else {
434 | // RULE
435 | if (!text.match(/[{}]/)) {
436 | // doesn't contain braces
437 | text += '{}'; // close it
438 | }
439 | }
440 | return {
441 | type: RULE,
442 | selector: text,
443 | };
444 | };
445 |
446 | // =====================================================================================================================
447 | // D E F I N I T I O N
448 | // =====================================================================================================================
449 | StyleEditor.defaultProps = {
450 | outputFormats: 'pretty',
451 | onChange: null,
452 | defaultValue: '',
453 | value: undefined,
454 | readOnly: false,
455 | };
456 | export default StyleEditor;
457 |
--------------------------------------------------------------------------------