Socket
Socket
Sign inDemoInstall

react-sane-contenteditable

Package Overview
Dependencies
7
Maintainers
4
Versions
23
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.4.0 to 1.4.1

.prettierrc.js

21

example/index.js

@@ -1,4 +0,4 @@

import ReactDOM from "react-dom";
import React, { Component } from "react";
import ContentEditable from "../src/react-sane-contenteditable";
import ReactDOM from 'react-dom';
import React, { Component } from 'react';
import ContentEditable from '../src/react-sane-contenteditable';

@@ -10,3 +10,3 @@ class App extends Component {

this.state = {
title: "Title here"
title: 'Title here',
};

@@ -19,2 +19,6 @@ }

handleKeyDown = (ev, value) => {
this.setState({ title: value });
};
render() {

@@ -24,2 +28,3 @@ return (

<ContentEditable
focus
tagName="h1"

@@ -32,2 +37,5 @@ className="my-class"

onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
caretPosition="end"
onKeyDown={this.handleKeyDown}
/>

@@ -39,4 +47,3 @@

fontSize: 14,
fontFamily:
"'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace"
fontFamily: "'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace",
}}

@@ -51,2 +58,2 @@ >

ReactDOM.render(<App />, document.getElementById("root"));
ReactDOM.render(<App />, document.getElementById('root'));

@@ -127,3 +127,3 @@ // For a detailed explanation regarding each configuration property, visit:

// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",
testEnvironment: "jest-environment-jsdom",

@@ -130,0 +130,0 @@ // Options that will be passed to the testEnvironment

@@ -29,11 +29,13 @@ function _typeof(obj) { 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); }

import React, { Component } from "react";
import PropTypes from "prop-types";
import { omit, isEqual, pick, without } from "lodash";
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { omit, isEqual, pick, without } from 'lodash';
var propTypes = {
content: PropTypes.string,
editable: PropTypes.bool,
focus: PropTypes.bool,
maxLength: PropTypes.number,
multiLine: PropTypes.bool,
sanitise: PropTypes.bool,
caretPosition: PropTypes.oneOf(['start', 'end']),
tagName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),

@@ -50,8 +52,10 @@ // The element to make contenteditable. Takes an element string ('div', 'span', 'h1') or a styled component

var defaultProps = {
content: "",
content: '',
editable: true,
focus: false,
maxLength: Infinity,
multiLine: false,
sanitise: true,
tagName: "div",
caretPosition: null,
tagName: 'div',
innerRef: function innerRef() {},

@@ -77,2 +81,22 @@ onBlur: function onBlur() {},

_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "setFocus", function () {
if (_this.props.focus && _this._element) {
_this._element.focus();
}
});
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "setCaret", function () {
var caretPosition = _this.props.caretPosition;
if (caretPosition && _this._element) {
var offset = caretPosition === 'end' ? 1 : 0;
var range = document.createRange();
var selection = window.getSelection();
range.setStart(_this._element, offset);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
});
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "_onChange", function (ev) {

@@ -95,4 +119,4 @@ var sanitise = _this.props.sanitise;

ev.preventDefault();
var text = ev.clipboardData.getData("text").substr(0, maxLength);
document.execCommand("insertText", false, text);
var text = ev.clipboardData.getData('text').substr(0, maxLength);
document.execCommand('insertText', false, text);

@@ -132,3 +156,3 @@ _this.props.onPaste(ev);

if (maxLength && !ev.metaKey && ev.which !== 8 && value.replace(/\s\s/g, " ").length >= maxLength) {
if (maxLength && !ev.metaKey && ev.which !== 8 && value.replace(/\s\s/g, ' ').length >= maxLength) {
ev.preventDefault(); // Call onKeyUp directly as ev.preventDefault() means that it will not be called

@@ -155,2 +179,8 @@

_createClass(ContentEditable, [{
key: "componentDidMount",
value: function componentDidMount() {
this.setFocus();
this.setCaret();
}
}, {
key: "componentWillReceiveProps",

@@ -161,3 +191,3 @@ value: function componentWillReceiveProps(nextProps) {

value: nextProps.content
}, this.forceUpdate);
});
}

@@ -168,6 +198,12 @@ }

value: function shouldComponentUpdate(nextProps) {
var propKeys = without(Object.keys(nextProps), "content");
var propKeys = without(Object.keys(nextProps), 'content');
return !isEqual(pick(nextProps, propKeys), pick(this.props, propKeys));
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate() {
this.setFocus();
this.setCaret();
}
}, {
key: "sanitiseValue",

@@ -185,14 +221,14 @@ value: function sanitiseValue(val) {

var value = val.replace(/&nbsp;/g, " ");
var value = val.replace(/&nbsp;/, ' ').replace(/[\u00a0\u2000-\u200b\u2028-\u2029\u202e-\u202f\u3000]/g, ' ');
if (multiLine) {
// replace any 2+ character whitespace (other than new lines) with a single space
value = value.replace(/[\t\v\f\r \u00a0\u2000-\u200b\u2028-\u2029\u3000]+/g, " ");
value = value.replace(/[\t\v\f\r ]+/g, ' ');
} else {
value = value.replace(/\s+/g, " ");
value = value.replace(/\s+/g, ' ');
}
return value.split("\n").map(function (line) {
return value.split('\n').map(function (line) {
return line.trim();
}).join("\n").replace(/\n{3,}/g, "\n\n") // replace 3+ line breaks with two
}).join('\n').replace(/\n{3,}/g, '\n\n') // replace 3+ line breaks with two
.trim().substr(0, maxLength);

@@ -224,3 +260,3 @@ }

style: _objectSpread({
whiteSpace: "pre-wrap"
whiteSpace: 'pre-wrap'
}, props.style),

@@ -227,0 +263,0 @@ contentEditable: editable,

{
"name": "react-sane-contenteditable",
"version": "1.4.0",
"version": "1.4.1",
"description": "React component with sane defaults to make any element contentEditable",

@@ -12,2 +12,3 @@ "main": "lib/index.js",

"contributors": [
"Nick Aspinall",
"Raphael Silva Cavalcanti",

@@ -35,10 +36,5 @@ "Jess Telford"

"enzyme-adapter-react-16": "^1.2.0",
"eslint": "^5.3.0",
"eslint-config-airbnb": "^17.0.0",
"eslint-plugin-babel": "^5.1.0",
"eslint-plugin-import": "^2.13.0",
"eslint-plugin-jest": "^21.18.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.10.0",
"jest": "^23.4.2",
"jsdom": "^11.12.0",
"prettier": "1.14.2",
"react-dom": "^16.3.2",

@@ -51,3 +47,4 @@ "rollup": "^0.58.2",

"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-serve": "^0.4.2"
"rollup-plugin-serve": "^0.4.2",
"styled-components": "^3.4.2"
},

@@ -60,3 +57,3 @@ "scripts": {

"test": "jest --watch",
"lint": "./node_modules/.bin/eslint src/*.js"
"lint": "./node_modules/prettier/bin-prettier.js --write src/**/*"
},

@@ -63,0 +60,0 @@ "bugs": {

import React from 'react';
import { mount, shallow } from 'enzyme';
import { JSDOM } from 'jsdom';
import styled from 'styled-components';
// SuT
import ContentEditable from '../react-sane-contenteditable';
// jsDOM
const doc = new JSDOM('<!doctype html><html><body></body></html>');
global.document = doc;
global.window = doc.defaultView;
global.window.getSelection = jest.fn(() => ({
addRange: jest.fn(),
removeAllRanges: jest.fn(),
}));
global.document.createRange = jest.fn(() => ({
collapse: jest.fn(),
setStart: jest.fn(),
}));
// Helpers
const focusThenBlur = (wrapper, element = 'div') => wrapper.find(element).simulate('focus').simulate('blur');
const focusThenBlur = (wrapper, element = 'div') =>
wrapper
.find(element)
.simulate('focus')
.simulate('blur');
// Styled components
const Wrapper = styled.div``;
describe('Default behaviour', () => {

@@ -19,3 +44,3 @@ it('renders a div by default', () => {

it('sets contenteditable', () => {
it('sets contentEditable', () => {
const wrapper = shallow(<ContentEditable />);

@@ -67,8 +92,42 @@ expect(wrapper.prop('contentEditable')).toBe(true);

// @todo: I think Enzyme converts props.ref to props.innerRef when props.styled={false}
it('props.styled={true} sets innerRef handler', () => {
const wrapper = mount(<ContentEditable />);
const wrapper = mount(<ContentEditable styled tagName={Wrapper} />);
expect(wrapper.prop('innerRef')).toEqual(expect.any(Function));
});
it('props.content change calls setState', () => {
const wrapper = mount(<ContentEditable content="" />);
const instance = wrapper.instance();
jest.spyOn(instance, 'setState');
wrapper.setProps({ content: 'foo' });
expect(instance.setState).toHaveBeenCalled();
});
it('props.focus sets focus on update', () => {
const wrapper = mount(<ContentEditable />);
const instance = wrapper.instance();
jest.spyOn(instance, 'setFocus');
wrapper.setProps({ focus: true });
expect(instance.setFocus).toHaveBeenCalled();
});
it('props.caretPosition sets selection on mount', () => {
mount(<ContentEditable caretPosition="start" />);
expect(global.window.getSelection).toHaveBeenCalled();
});
it('props.caretPosition sets selection on update', () => {
const wrapper = mount(<ContentEditable />);
const instance = wrapper.instance();
jest.spyOn(instance, 'setCaret');
wrapper.setProps({ caretPosition: 'end' });
expect(instance.setCaret).toHaveBeenCalled();
});
it('shouldComponentUpdate returns false when props are the same', () => {

@@ -172,3 +231,2 @@ const wrapper = mount(<ContentEditable multiLine />);

// @todo: ASCII spaces and feeds should probably be replaced regardless of multiLine
it('replaces ASCII spaces and feeds', () => {

@@ -178,3 +236,3 @@ const mockHandler = jest.fn();

wrapper.instance()._element.innerText = `foo\f\f bar\r\r baz\t\t qux\v\v quux`;
wrapper.instance()._element.innerText = 'foo\f\f bar\r\r baz\t\t qux\v\v quux';
focusThenBlur(wrapper);

@@ -184,6 +242,5 @@ expect(wrapper.state('value')).toEqual('foo bar baz qux quux');

// @todo: Unicode spaces should probably be replaced regardless of multiLine
it('replaces unicode spaces', () => {
const mockHandler = jest.fn();
const wrapper = mount(<ContentEditable multiLine content="foo" onChange={mockHandler} />);
const wrapper = mount(<ContentEditable content="foo" onChange={mockHandler} />);
const unicodeChars = [

@@ -205,6 +262,8 @@ '\u00a0',

'\u2029',
'\u202e',
'\u202f',
'\u3000',
].join('');
wrapper.instance()._element.innerText = `foo${unicodeChars}bar`;
wrapper.instance()._element.innerText = `foo ${unicodeChars}bar`;
focusThenBlur(wrapper);

@@ -215,40 +274,2 @@ expect(wrapper.state('value')).toEqual('foo bar');

xdescribe('Failing tests to fix in component', () => {
// @todo @fixme: This test should probably be fixed and merged into the previous test: 'replaces unicode spaces when props.multiLine'
it('replaces unicode spaces', () => {
const mockHandler = jest.fn();
const wrapper = mount(<ContentEditable multiLine content="foo" onChange={mockHandler} />);
const unicodeChars = [
'\u200c',
'\u200d',
'\u200e',
'\u200f',
'\u202a',
'\u202b',
'\u202c',
'\u202d',
'\u202e',
'\u202f',
'\u2060',
'\u2061',
'\u2062',
'\u2063',
'\u2064',
'\u2065',
'\u2066',
'\u2067',
'\u2068',
'\u2069',
'\u206a',
'\u206b',
'\u206c',
'\u206d',
'\u206e',
'\u206f',
].join('');
wrapper.instance()._element.innerText = `foo${unicodeChars}bar`;
focusThenBlur(wrapper);
expect(wrapper.state('value')).toEqual('foo bar');
});
// @todo @fixme: Component should probably be fixed so that this test passes, given that it uses dangerouslySetInnerHTML

@@ -261,3 +282,3 @@ // the naming of props.sanitise suggests that it will protect against this.

wrapper.instance()._element.innerText = 'foo <script>console.log(\'XSS vulnerability\')</script>';
wrapper.instance()._element.innerText = "foo <script>console.log('XSS vulnerability')</script>";
focusThenBlur(wrapper);

@@ -274,3 +295,2 @@ expect(wrapper.state('value')).toEqual(content);

const wrapper = mount(<ContentEditable innerRef={mockHandler} />);
wrapper.render();
expect(mockHandler).toHaveBeenCalled();

@@ -286,5 +306,7 @@ });

focusThenBlur(wrapper);
expect(mockHandler).toHaveBeenCalledWith(expect.objectContaining({
type: 'blur',
}));
expect(mockHandler).toHaveBeenCalledWith(
expect.objectContaining({
type: 'blur',
}),
);
});

@@ -360,7 +382,9 @@

wrapper.find('div').simulate('paste', { clipboardData: { getData: mockGetClipboardData } });
expect(mockOnPaste).toHaveBeenCalledWith(expect.objectContaining({
type: 'paste',
nativeEvent: expect.any(Object),
}));
expect(mockOnPaste).toHaveBeenCalledWith(
expect.objectContaining({
type: 'paste',
nativeEvent: expect.any(Object),
}),
);
});
});

@@ -1,4 +0,4 @@

import React, { Component } from "react";
import PropTypes from "prop-types";
import { omit, isEqual, pick, without } from "lodash";
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { omit, isEqual, pick, without } from 'lodash';

@@ -8,5 +8,7 @@ const propTypes = {

editable: PropTypes.bool,
focus: PropTypes.bool,
maxLength: PropTypes.number,
multiLine: PropTypes.bool,
sanitise: PropTypes.bool,
caretPosition: PropTypes.oneOf(['start', 'end']),
tagName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), // The element to make contenteditable. Takes an element string ('div', 'span', 'h1') or a styled component

@@ -18,12 +20,14 @@ innerRef: PropTypes.func,

onChange: PropTypes.func,
styled: PropTypes.bool // If element is a styled component (uses innerRef instead of ref)
styled: PropTypes.bool, // If element is a styled component (uses innerRef instead of ref)
};
const defaultProps = {
content: "",
content: '',
editable: true,
focus: false,
maxLength: Infinity,
multiLine: false,
sanitise: true,
tagName: "div",
caretPosition: null,
tagName: 'div',
innerRef: () => {},

@@ -34,3 +38,3 @@ onBlur: () => {},

onChange: () => {},
styled: false
styled: false,
};

@@ -43,9 +47,14 @@

this.state = {
value: props.content
value: props.content,
};
}
componentDidMount() {
this.setFocus();
this.setCaret();
}
componentWillReceiveProps(nextProps) {
if (nextProps.content !== this.sanitiseValue(this.state.value)) {
this.setState({ value: nextProps.content }, this.forceUpdate);
this.setState({ value: nextProps.content });
}

@@ -55,6 +64,32 @@ }

shouldComponentUpdate(nextProps) {
const propKeys = without(Object.keys(nextProps), "content");
const propKeys = without(Object.keys(nextProps), 'content');
return !isEqual(pick(nextProps, propKeys), pick(this.props, propKeys));
}
componentDidUpdate() {
this.setFocus();
this.setCaret();
}
setFocus = () => {
if (this.props.focus && this._element) {
this._element.focus();
}
};
setCaret = () => {
const { caretPosition } = this.props;
if (caretPosition && this._element) {
const offset = caretPosition === 'end' ? 1 : 0;
const range = document.createRange();
const selection = window.getSelection();
range.setStart(this._element, offset);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
};
sanitiseValue(val) {

@@ -68,19 +103,16 @@ const { maxLength, multiLine, sanitise } = this.props;

// replace encoded spaces
let value = val.replace(/&nbsp;/g, " ");
let value = val.replace(/&nbsp;/, ' ').replace(/[\u00a0\u2000-\u200b\u2028-\u2029\u202e-\u202f\u3000]/g, ' ');
if (multiLine) {
// replace any 2+ character whitespace (other than new lines) with a single space
value = value.replace(
/[\t\v\f\r \u00a0\u2000-\u200b\u2028-\u2029\u3000]+/g,
" "
);
value = value.replace(/[\t\v\f\r ]+/g, ' ');
} else {
value = value.replace(/\s+/g, " ");
value = value.replace(/\s+/g, ' ');
}
return value
.split("\n")
.split('\n')
.map(line => line.trim())
.join("\n")
.replace(/\n{3,}/g, "\n\n") // replace 3+ line breaks with two
.join('\n')
.replace(/\n{3,}/g, '\n\n') // replace 3+ line breaks with two
.trim()

@@ -106,4 +138,4 @@ .substr(0, maxLength);

ev.preventDefault();
const text = ev.clipboardData.getData("text").substr(0, maxLength);
document.execCommand("insertText", false, text);
const text = ev.clipboardData.getData('text').substr(0, maxLength);
document.execCommand('insertText', false, text);

@@ -140,8 +172,3 @@ this.props.onPaste(ev);

// Ensure we don't exceed `maxLength` (keycode 8 === backspace)
if (
maxLength &&
!ev.metaKey &&
ev.which !== 8 &&
value.replace(/\s\s/g, " ").length >= maxLength
) {
if (maxLength && !ev.metaKey && ev.which !== 8 && value.replace(/\s\s/g, ' ').length >= maxLength) {
ev.preventDefault();

@@ -158,13 +185,7 @@ // Call onKeyUp directly as ev.preventDefault() means that it will not be called

// this._onKeyDown can't be moved in it's entirety to onKeyUp as we lose the opportunity to preventDefault
this.props.onKeyDown(ev, this._element.innerText)
this.props.onKeyDown(ev, this._element.innerText);
};
render() {
const {
tagName: Element,
content,
editable,
styled,
...props
} = this.props;
const { tagName: Element, content, editable, styled, ...props } = this.props;

@@ -179,3 +200,3 @@ return (

props.innerRef(c);
}
},
}

@@ -186,5 +207,5 @@ : {

props.innerRef(c);
}
},
})}
style={{ whiteSpace: "pre-wrap", ...props.style }}
style={{ whiteSpace: 'pre-wrap', ...props.style }}
contentEditable={editable}

@@ -191,0 +212,0 @@ key={Date()}

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc