├── examples ├── .babelrc ├── dist │ └── index.html ├── index.js ├── webpack.config.babel.js ├── Example.js └── webpack.config.live.babel.js ├── .storybook ├── .babelrc └── main.js ├── src ├── components │ ├── ReactTrixRTEInput │ │ ├── ReactTrixRTEInput.style.scss │ │ ├── index.js │ │ ├── constants.js │ │ ├── ReactTrixRTEInput.test.js │ │ └── ReactTrixRTEInput.jsx │ ├── ReactTrixRTEToolbar │ │ ├── index.js │ │ ├── ToolbarComponent │ │ │ ├── ToolbarButton │ │ │ │ ├── index.js │ │ │ │ ├── ToolbarButton.test.js │ │ │ │ └── ToolbarButton.jsx │ │ │ ├── ToolbarSpacer │ │ │ │ ├── index.js │ │ │ │ ├── ToolbarSpacer.jsx │ │ │ │ └── ToolbarSpacer.test.js │ │ │ ├── ToolbarLinkDialog │ │ │ │ ├── index.js │ │ │ │ ├── ToolbarLinkDialog.test.js │ │ │ │ └── ToolbarLinkDialog.jsx │ │ │ └── ToolbarButtonGroup │ │ │ │ ├── index.js │ │ │ │ ├── ToolbarButtonGroup.test.js │ │ │ │ └── ToolbarButtonGroup.jsx │ │ ├── ReactTrixRTEToolbar.test.js │ │ ├── ReactTrixRTEToolbar.jsx │ │ └── constants.js │ └── Shared │ │ └── utils.js └── index.js ├── index.js ├── .babelrc ├── config ├── jest │ ├── fileTransform.js │ ├── setup.js │ └── cssTransform.js └── polyfills.js ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .eslintrc ├── jest.config.js ├── CONTRIBUTING.md ├── LICENSE ├── webpack.config.babel.js ├── CHANGELOG.md ├── package.json ├── stories ├── 1-ReactTrixWithCustomToolbar.stories.js └── 0-ReactTrixInputRTE.stories.js ├── readme.md └── dist └── index.js /examples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEInput/ReactTrixRTEInput.style.scss: -------------------------------------------------------------------------------- 1 | @import "trix/dist/trix"; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // require('./dist/manifest'); 2 | // require('./dist/vendor'); 3 | module.exports = require('./dist/index'); 4 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEInput/index.js: -------------------------------------------------------------------------------- 1 | import ReactTrixRTEInput from './ReactTrixRTEInput'; 2 | 3 | export default ReactTrixRTEInput; 4 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/index.js: -------------------------------------------------------------------------------- 1 | import ReactTrixRTEToolbar from "./ReactTrixRTEToolbar"; 2 | 3 | export default ReactTrixRTEToolbar; 4 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarButton/index.js: -------------------------------------------------------------------------------- 1 | import ToolbarButton from "./ToolbarButton"; 2 | 3 | export default ToolbarButton; 4 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarSpacer/index.js: -------------------------------------------------------------------------------- 1 | import ToolbarSpacer from "./ToolbarSpacer"; 2 | 3 | export default ToolbarSpacer; 4 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarLinkDialog/index.js: -------------------------------------------------------------------------------- 1 | import ToolbarLinkDialog from "./ToolbarLinkDialog" 2 | 3 | export default ToolbarLinkDialog; 4 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarButtonGroup/index.js: -------------------------------------------------------------------------------- 1 | import ToolbarButtonGroup from "./ToolbarButtonGroup"; 2 | 3 | export default ToolbarButtonGroup; 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-object-rest-spread", 5 | "@babel/plugin-transform-react-jsx" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEInput/constants.js: -------------------------------------------------------------------------------- 1 | export const RAILS_SERVICE_BLOB_URL = "/rails/active_storage/blobs/:signed_id/*filename"; 2 | export const RAILS_DIRECT_UPLOADS_URL = "/rails/active_storage/direct_uploads"; 3 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | process(src, filename) { 6 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarSpacer/ToolbarSpacer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function ToolbarSpacer() { 4 | return(); 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Run Jest 11 | uses: stefanoeb/jest-action@1.0.2 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactTrixRTEInput from "./components/ReactTrixRTEInput"; 2 | import ReactTrixRTEToolbar from "./components/ReactTrixRTEToolbar/ReactTrixRTEToolbar"; 3 | 4 | export { 5 | ReactTrixRTEInput, 6 | ReactTrixRTEToolbar, 7 | }; 8 | -------------------------------------------------------------------------------- /config/jest/setup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | // React 16 Enzyme adapter 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | // Make Enzyme functions available in all test files without importing 6 | -------------------------------------------------------------------------------- /examples/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarSpacer/ToolbarSpacer.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import ToolbarSpacer from "./ToolbarSpacer"; 4 | 5 | describe("ToolbarSpacer", () => { 6 | it("renders ToolbarSpacer without crashing", () => { 7 | const wrapper = shallow(); 8 | expect(wrapper).toHaveLength(1); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarLinkDialog/ToolbarLinkDialog.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import ToolbarLinkDialog from "./ToolbarLinkDialog"; 4 | 5 | describe("ToolbarLinkDialog", () => { 6 | it("renders the ToolbarLinkDialog without crashing", () => { 7 | const wrapper = shallow(); 8 | expect(wrapper).toHaveLength(1); 9 | }) 10 | }); 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | yarn-error.log 4 | 5 | # Coverage directory used by tools like istanbul 6 | coverage 7 | .nyc_output 8 | 9 | # Dependency directories 10 | node_modules 11 | 12 | # npm package lock 13 | package-lock.json 14 | yarn.lock 15 | 16 | # project files 17 | src 18 | test 19 | examples 20 | CHANGELOG.md 21 | .travis.yml 22 | .editorconfig 23 | .eslintignore 24 | .eslintrc 25 | .babelrc 26 | .gitignore 27 | 28 | 29 | # lock files 30 | package-lock.json 31 | yarn.lock 32 | 33 | # others 34 | .DS_Store 35 | .idea 36 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarButton/ToolbarButton.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import ToolbarButton from './ToolbarButton'; 4 | 5 | describe('ToolbarButton', () => { 6 | it("renders the ToolbarButton without crashing", () => { 7 | const wrapper = shallow( 8 | 14 | ); 15 | expect(wrapper).toHaveLength(1); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/Shared/utils.js: -------------------------------------------------------------------------------- 1 | export const groupBy = (collection, prop) => { 2 | return Object.keys(collection).reduce((groups, item) => { 3 | const groupKey = collection[item][prop]; 4 | groups[groupKey] = groups[groupKey] || [] 5 | groups[groupKey].push(collection[item]) 6 | return groups 7 | }, {}); 8 | } 9 | 10 | export const getUniqInputId = (prefix = 'react-trix-rte-input-') => { 11 | const dateTimestamp = new Date().getTime(); 12 | const randomSalt = Math.random().toFixed(9).slice(2) 13 | return `${prefix}${dateTimestamp}-${randomSalt}` 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true, 7 | "jest": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 7, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "plugins": [ 17 | "react" 18 | ], 19 | "extends": ["eslint:recommended", "plugin:react/recommended"], 20 | "rules": { 21 | "no-multiple-empty-lines": ["error", { "max": 2 }], 22 | "indent": ["error", 2, { "SwitchCase": 1 }], 23 | "no-console": 1 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Example from './Example'; 5 | import { AppContainer } from 'react-hot-loader'; 6 | // AppContainer is a necessary wrapper component for HMR 7 | 8 | const render = (Component) => { 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | }; 16 | 17 | render(Example); 18 | 19 | // Hot Module Replacement API 20 | if (module.hot) { 21 | module.hot.accept('./Example', () => { 22 | render(Example) 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coverageReporters: [ 3 | 'json', 4 | 'lcov', 5 | 'text-summary' 6 | ], 7 | moduleFileExtensions: [ 8 | 'js', 9 | 'jsx', 10 | 'scss' 11 | ], 12 | modulePaths: [ 13 | './src' 14 | ], 15 | setupFiles: [ 16 | '/config/jest/setup.js' 17 | ], 18 | transform: { 19 | '^.+\\.(js|jsx)$': '/node_modules/babel-jest', 20 | '^.+\\.css$': '/config/jest/cssTransform.js', 21 | '^(?!.*\\.(js|jsx|css|json)$)': '/config/jest/fileTransform.js' 22 | }, 23 | transformIgnorePatterns: [ 24 | '[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$' 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarButtonGroup/ToolbarButtonGroup.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import ToolbarButtonGroup from "./ToolbarButtonGroup"; 4 | 5 | describe("ToolbarButtonGroup", () => { 6 | let defaultProps = { 7 | groupName: "text-tools", 8 | toolbarActionOptions: [{ 9 | type: "button", 10 | classNames: "trix-button trix-button--icon trix-button--icon-bold", 11 | languageKey: "bold", 12 | tabIndex: "-1", 13 | trixButtonGroup: "text-tools", 14 | }] 15 | } 16 | 17 | it("renders the ToolbarButtonGroup without crashing", () => { 18 | const wrapper = shallow(); 19 | expect(wrapper).toHaveLength(1); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarButtonGroup/ToolbarButtonGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ToolbarButton from "../ToolbarButton"; 4 | import { TOOLBAR_ACTION_GROUP_OPTS } from "../../constants"; 5 | 6 | function ToolbarButtonGroup(props) { 7 | const { groupName, toolbarActionOptions } = props; 8 | 9 | function renderToolbarActions() { 10 | const toolbarButtonsHTML = [] 11 | toolbarActionOptions.forEach((toolbarActionOption, index) => { 12 | toolbarButtonsHTML.push() 13 | }) 14 | 15 | return toolbarButtonsHTML 16 | } 17 | 18 | return ( 19 | 20 | {renderToolbarActions()} 21 | 22 | ) 23 | } 24 | 25 | ToolbarButtonGroup.propTypes = { 26 | groupName: PropTypes.string.isRequired, 27 | toolbarActionOptions: PropTypes.array.isRequired 28 | } 29 | 30 | export default ToolbarButtonGroup; 31 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | stories: ['../stories/**/*.stories.js'], 5 | addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-cssresources'], 6 | webpackFinal: (config) => { 7 | return { 8 | ...config, 9 | module: { 10 | ...config.module, 11 | rules: [ 12 | { 13 | test: /\.(scss)$/, 14 | loader: 'style-loader!css-loader!sass-loader' 15 | }, 16 | { 17 | test: /\.(js|jsx)$/, 18 | exclude: /node_modules/, 19 | include: path.join(__dirname, '../stories'), 20 | use: [ 21 | { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-env', '@babel/preset-react'] 25 | } 26 | } 27 | ] 28 | } 29 | ] 30 | }, 31 | resolve: { 32 | extensions: ['.js', '.jsx', '.scss'] 33 | }, 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love contributions from everyone. Please open a issue for discussion or raise a 4 | pull request with the changes you need in the package. 5 | 6 | ## Contributing Code 7 | To start contributing to the package. We should follow following steps. 8 | 9 | - Fork the repository first to your GitHub account. 10 | - Install all package dependencies by executing `yarn/npm install` in your console. 11 | - Add test cases for the changes made in the pull request. Make sure they are passing by running `yarn/npm test`. 12 | - Update documentation for the changes made if needed. 13 | - Add a simple story to demonstrate the change for the end user if needed. 14 | - Add a changelog for the new feature or fixes. 15 | 16 | Push to your fork. Write a [good commit message][commit]. Submit a pull request. 17 | 18 | [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 19 | 20 | Please do not self reject your thoughts. Let's discuss even smallest feature request as well and make 21 | this package better for everybody's use. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Abhay Nikam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; //eslint-disable-line 3 | import CleanWebpackPlugin from 'clean-webpack-plugin'; 4 | 5 | export default () => ({ 6 | mode: 'production', 7 | entry: { 8 | index: path.join(__dirname, './index.js') 9 | }, 10 | 11 | output: { 12 | filename: 'bundle.js', 13 | path: path.resolve(__dirname, 'dist') 14 | }, 15 | 16 | module: { 17 | rules: [ 18 | { 19 | test: /.jsx?$/, 20 | exclude: /node_modules/, 21 | 22 | use: [ 23 | { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: ['@babel/preset-env', '@babel/preset-react'] 27 | } 28 | } 29 | ] 30 | }, 31 | { 32 | test: /\.(scss)$/, 33 | loader: 'style-loader!css-loader!sass-loader' 34 | } 35 | ] 36 | }, 37 | 38 | resolve: { 39 | extensions: ['.js', '.jsx', '.scss'] 40 | }, 41 | 42 | plugins: [ 43 | // Clean dist folder 44 | new CleanWebpackPlugin(['./dist/build.js']) 45 | ] 46 | }); 47 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import CleanWebpackPlugin from 'clean-webpack-plugin'; 3 | const packageJson = require('./package.json'); 4 | 5 | export default () => ({ 6 | mode: 'production', 7 | entry: { 8 | index: path.join(__dirname, 'src/index.js') 9 | }, 10 | 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: '[name].js', 14 | library: packageJson.name, 15 | libraryTarget: 'umd', 16 | globalObject: 'this' 17 | }, 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /.jsx?$/, 23 | exclude: /node_modules/, 24 | include: path.join(__dirname, 'src'), 25 | use: [ 26 | { 27 | loader: 'babel-loader', 28 | options: { 29 | presets: ['@babel/preset-env', '@babel/preset-react'] 30 | } 31 | } 32 | ] 33 | }, 34 | { 35 | test: /\.(scss)$/, 36 | loader: 'style-loader!css-loader!sass-loader' 37 | } 38 | ] 39 | }, 40 | 41 | resolve: { 42 | extensions: ['.js', '.jsx', '.scss'] 43 | }, 44 | 45 | externals: { 46 | react: 'react', 47 | reactDOM: 'react-dom' 48 | }, 49 | 50 | plugins: [new CleanWebpackPlugin(['dist/*.*'])], 51 | optimization: { 52 | splitChunks: { 53 | name: 'vendor', 54 | minChunks: 2 55 | } 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /examples/Example.js: -------------------------------------------------------------------------------- 1 | import Trix from 'trix'; 2 | import React from 'react'; 3 | 4 | import { ReactTrixRTEInput, ReactTrixRTEToolbar } from '../src/index'; 5 | 6 | class Example extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | handleChange(e, newValue) { 12 | console.log(newValue); 13 | } 14 | 15 | handleFocus(event) { 16 | console.log("On Focus.."); 17 | } 18 | 19 | render() { 20 | const customOption = { 21 | table: { 22 | type: "button", 23 | classNames: "trix-button trix-button--icon trix-button--icon-bold", 24 | languageKey: "table", 25 | tabIndex: "-1", 26 | trixButtonGroup: "text-tools", 27 | elementProps: { 28 | onClick: () => { alert("clicked") }, 29 | }, 30 | data: { 31 | trixAttribute: "table", 32 | trixKey: "ta", 33 | }, 34 | } 35 | }; 36 | 37 | return ( 38 |
39 | 43 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | export default Example; 55 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarButton/ToolbarButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { TOOLBAR_LANGUAGE_OPTS } from "../../constants"; 5 | 6 | function ToolbarButton(props) { 7 | const { type, classNames, data, tabIndex, languageKey, elementProps } = props; 8 | 9 | let dataAttributeOptions = {}; 10 | if(data) { 11 | const { trixAttribute, trixKey, trixAction } = data; 12 | if(trixAttribute) dataAttributeOptions["data-trix-attribute"] = trixAttribute; 13 | if(trixAction) dataAttributeOptions["data-trix-action"] = trixAction; 14 | if(trixAttribute) dataAttributeOptions["data-trix-key"] = trixKey; 15 | } 16 | 17 | return ( 18 | 28 | ) 29 | } 30 | 31 | ToolbarButton.defaultProps = { 32 | elementProps: {}, 33 | } 34 | 35 | ToolbarButton.propTypes = { 36 | type: PropTypes.string.isRequired, 37 | classNames: PropTypes.string.isRequired, 38 | data: PropTypes.object, 39 | tabIndex: PropTypes.string, 40 | languageKey: PropTypes.string.isRequired, 41 | elementProps: PropTypes.object, 42 | } 43 | 44 | export default ToolbarButton; 45 | 46 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ToolbarComponent/ToolbarLinkDialog/ToolbarLinkDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TOOLBAR_LANGUAGE_OPTS } from "../../constants"; 3 | 4 | export default function ToolbarLinkDialog() { 5 | return ( 6 |
10 |
15 |
16 | 25 |
26 | 32 | 38 |
39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ReactTrixRTEToolbar.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import ReactTrixRTEToolbar from './ReactTrixRTEToolbar'; 4 | import ToolbarButtonGroup from "./ToolbarComponent/ToolbarButtonGroup"; 5 | import ToolbarSpacer from "./ToolbarComponent/ToolbarSpacer"; 6 | 7 | describe('ReactTrixRTEToolbar', () => { 8 | it('renders without crashing', () => { 9 | const wrapper = shallow(); 10 | expect(wrapper).toHaveLength(1); 11 | }); 12 | 13 | it('renders with toolbarId', () => { 14 | const wrapper = shallow(); 15 | expect(wrapper.find('trix-toolbar').props().id).toEqual("react-trix-rte-editor"); 16 | }); 17 | 18 | it('renders toolbar with specified toolbarActions', () => { 19 | let toolbarActions = ["bold", "indent", "undo"] 20 | const wrapper = shallow(); 21 | expect(wrapper.find(ToolbarButtonGroup)).toHaveLength(3); 22 | }); 23 | 24 | it('renders toolbar without grouping actions', () => { 25 | const wrapper = shallow(); 26 | expect(wrapper.find('.trix-button-group')).toHaveLength(1); 27 | }); 28 | 29 | it('renders toolbar with grouping actions', () => { 30 | const wrapper = shallow(); 31 | expect(wrapper.find(ToolbarButtonGroup)).toHaveLength(4); 32 | expect(wrapper.find(ToolbarSpacer)).toHaveLength(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /examples/webpack.config.live.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | 4 | export default () => ({ 5 | mode: 'development', 6 | entry: [ 7 | 'react-hot-loader/patch', 8 | // activate HMR for React 9 | 10 | 'webpack-dev-server/client?http://localhost:8080', 11 | // bundle the client for webpack-dev-server 12 | // and connect to the provided endpoint 13 | 14 | 'webpack/hot/only-dev-server', 15 | // bundle the client for hot reloading 16 | // only- means to only hot reload for successful updates 17 | 18 | './examples/index.js' 19 | // the entry point of our app 20 | ], 21 | 22 | output: { 23 | filename: 'bundle.js', 24 | path: path.resolve(__dirname, 'dist'), 25 | publicPath: '/' 26 | // necessary for HMR to know where to load the hot update chunks 27 | }, 28 | 29 | devtool: 'inline-source-map', 30 | 31 | devServer: { 32 | hot: true, 33 | // enable HMR on the server 34 | 35 | contentBase: path.resolve(__dirname, 'dist'), 36 | // match the output path 37 | 38 | publicPath: '/', 39 | // match the output `publicPath` 40 | 41 | stats: 'minimal' 42 | }, 43 | 44 | module: { 45 | rules: [ 46 | { 47 | test: /.jsx?$/, 48 | exclude: /node_modules/, 49 | use: [ 50 | { 51 | loader: 'babel-loader', 52 | options: { 53 | presets: ['@babel/preset-env', '@babel/preset-react'] 54 | } 55 | } 56 | ] 57 | }, 58 | { 59 | test: /\.(scss)$/, 60 | loader: 'style-loader!css-loader!sass-loader' 61 | } 62 | ] 63 | }, 64 | 65 | resolve: { 66 | extensions: ['.js', '.jsx', '.scss'] 67 | }, 68 | 69 | plugins: [new webpack.HotModuleReplacementPlugin()], 70 | optimization: { 71 | namedModules: true 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEInput/ReactTrixRTEInput.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import ReactTrixRTEInput from './ReactTrixRTEInput'; 4 | import { RAILS_DIRECT_UPLOADS_URL, RAILS_SERVICE_BLOB_URL } from "./constants"; 5 | 6 | describe('ReactTrixRTEInput', () => { 7 | it('renders without crashing', () => { 8 | const wrapper = shallow(); 9 | expect(wrapper).toHaveLength(1); 10 | }); 11 | 12 | it('renders with toolbarId', () => { 13 | const wrapper = shallow(); 14 | expect(wrapper.find('trix-editor').props().toolbar).toEqual("react-trix-rte-editor"); 15 | }); 16 | 17 | it('renders with default value', () => { 18 | const wrapper = shallow(); 19 | expect(wrapper.find('input').props().value).toEqual("

React Trix RTE

"); 20 | }); 21 | 22 | it('renders with rails direct upload URL', () => { 23 | const wrapper = shallow(); 24 | expect(wrapper.find('trix-editor').props()['data-direct-upload-url']).toEqual(RAILS_DIRECT_UPLOADS_URL); 25 | expect(wrapper.find('trix-editor').props()['data-blob-url-template']).toEqual(RAILS_SERVICE_BLOB_URL); 26 | }); 27 | 28 | it('renders with custom rails direct upload URL', () => { 29 | const wrapper = shallow(); 30 | expect(wrapper.find('trix-editor').props()['data-direct-upload-url']).toEqual("A"); 31 | expect(wrapper.find('trix-editor').props()['data-blob-url-template']).toEqual("B"); 32 | }); 33 | 34 | it('renders with placeholder', () => { 35 | const wrapper = shallow(); 36 | expect(wrapper.find('trix-editor').props().placeholder).toEqual("Enter the description"); 37 | 38 | const withoutPlaceholderWrapper = shallow(); 39 | expect(withoutPlaceholderWrapper.find('trix-editor').props().placeholder).toEqual(undefined); 40 | }); 41 | 42 | it('renders with autofocus', () => { 43 | const wrapper = shallow(); 44 | expect(wrapper.find('trix-editor').props().autofocus).toEqual(true); 45 | }); 46 | 47 | it('renders with custom class via className prop', () => { 48 | const wrapper = shallow(); 49 | expect(wrapper.find('trix-editor').props().class).toEqual('tx-editor') 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## master (unreleased) 4 | 5 | ## 1.0.16 (14-Jan-2022) 6 | * Fixes multiple instances of Trix editor triggering same onChange event. Fixes #47. ([@abhaynikam][]) 7 | 8 | ## 1.0.15 (18-Nov-2021) 9 | * Enable custom URL for Rails direct upload with `railsDirectUploadUrl` and `railsBlobUrl` prop in `ReactTrixRTEInput`. Refer: [#43](/issues/43). ([@mech][]) 10 | * Fixes conflicting Trix editor id ([@rjclaasen ][]) 11 | 12 | ## 1.0.14 (21-July-2021) 13 | * Fixes onAttachmentRemove trix event listener. Fixes #36. ([@radhenazarkar][]) 14 | * Enable `className` prop in `ReactTrixRTEInput`. Refer: [#30](/issues/30). ([@alejo4373][]) 15 | 16 | ## 1.0.13 (29-March-2021) 17 | * Fixes issue when binding Trix events on the DOM element when React has not rendered the component yet and no DOM element reference is present. ([@CUnknown][]) 18 | * Fixes ReactTrixRTEToolbar component crashing when using `toolbarActions`. Refer: (#24). ([@alejo4373][]) 19 | * Removes Ramda dependency. ([@alejo4373][]) 20 | 21 | ## 1.0.11 (11-March-2021) 22 | * Fixes the ReactTrixRTEToolbar breaking because of missing dependency - ([@Axxxx0n][]) 23 | 24 | ## 1.0.10 (11-February-2021) 25 | * Bump React version to 1.17.1. ([@abhaynikam][]) 26 | * Bump Trix version to 1.13.1. ([@abhaynikam][]) 27 | * Update package.json to move React to peer dependencies. ([@abhaynikam][]) 28 | 29 | ## 1.0.9 (18-January-2021) 30 | * Adds `elementProps` option to add custom props to the ToolbarButton. ([@abhaynikam][]) 31 | 32 | ## 1.0.8 (18-January-2021) 33 | * Adds option to override or add custom toolbar actions. ([@abhaynikam][]) 34 | 35 | ## 1.0.7 (02-December-2020) 36 | * Remove 'unused' import of Trix to prevent webpack packaging conflicts with projects. ([@CUnknown][]) 37 | * Support ID and Name input attribute as component props. ([@CUnknown][]) 38 | 39 | ## 1.0.6 (17-September-2020) 40 | 41 | * Bump trix version `1.2.3` -> `1.2.4`. ([@abhaynikam][]) 42 | 43 | ## 1.0.5 (29-July-2020) 44 | 45 | * Add ability to allow Rails direct uploading via Trix Editor. ([@abhaynikam][]) 46 | * Add options to configure placeholder and autofocus for Trix Editor. ([@abhaynikam][]) 47 | 48 | ## 1.0.4 (30-March-2020) 49 | 50 | * Create React Trix RTE NPM package. ([@abhaynikam][]) 51 | 52 | [@abhaynikam]: https://github.com/abhaynikam 53 | [@CUnknown]: https://github.com/CUnknown 54 | [@Axxxx0n]: https://github.com/Axxxx0n 55 | [@alejo4373]: https://github.com/alejo4373 56 | [@radhenazarkar]: https://github.com/radhenazarkar 57 | [@mech]: https://github.com/mech 58 | [@rjclaasen]: https://github.com/rjclaasen 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-trix-rte", 3 | "version": "1.0.15", 4 | "description": "React wrapper for Trix rich text editor created by Basecamp", 5 | "main": "./index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.babel.js", 8 | "build-examples": "webpack --config examples/webpack.config.babel.js --progress", 9 | "clean": "rm -rf dist coverage", 10 | "coverage": "jest --coverage", 11 | "lint": "eslint ./src", 12 | "prepublish": "npm run clean && npm run test && npm run build", 13 | "start": "webpack-dev-server --config examples/webpack.config.live.babel.js", 14 | "test": "npm run lint && npm run coverage", 15 | "storybook": "start-storybook -p 6006", 16 | "build-storybook": "build-storybook", 17 | "deploy-storybook": "storybook-to-ghpages" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/abhaynikam/react-trix-rte.git" 22 | }, 23 | "keywords": [ 24 | "React", 25 | "Trix" 26 | ], 27 | "author": "Abhay Nikam", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/abhaynikam/react-trix-rte/issues" 31 | }, 32 | "homepage": "https://abhaynikam.github.io/react-trix-rte/", 33 | "dependencies": { 34 | "trix": "^1.3.1" 35 | }, 36 | "peerDependencies ": { 37 | "react": "^17.0.1", 38 | "react-dom": "^17.0.1" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.1.2", 42 | "@babel/preset-env": "^7.1.0", 43 | "@babel/preset-react": "^7.0.0", 44 | "@babel/register": "^7.0.0", 45 | "@storybook/addon-actions": "^6.2.9", 46 | "@storybook/addon-cssresources": "^6.2.9", 47 | "@storybook/addon-links": "^6.2.9", 48 | "@storybook/addons": "^6.2.9", 49 | "@storybook/react": "^6.2.9", 50 | "@storybook/storybook-deployer": "^2.8.5", 51 | "babel-eslint": "^10.0.1", 52 | "babel-jest": "^23.6.0", 53 | "babel-loader": "^8.0.4", 54 | "chai": "^4.1.2", 55 | "clean-webpack-plugin": "^0.1.16", 56 | "css-loader": "^1.0.1", 57 | "enzyme": "^3.3.0", 58 | "enzyme-adapter-react-16": "^1.1.1", 59 | "eslint": "^5.8.0", 60 | "eslint-plugin-react": "^7.4.0", 61 | "gh-pages": "^2.2.0", 62 | "jest-cli": "^23.6.0", 63 | "node-sass": "^7.0.0", 64 | "prop-types": "^15.6.0", 65 | "react-hot-loader": "next", 66 | "react-test-renderer": "^16.0.0", 67 | "regenerator-runtime": "^0.12.1", 68 | "sass-loader": "^7.0.1", 69 | "style-loader": "^0.23.1", 70 | "webpack": "^4.42.1", 71 | "webpack-cli": "^3.3.11", 72 | "webpack-dev-server": "^3.10.3" 73 | }, 74 | "resolutions": { 75 | "babel-core": "7.0.0-bridge.0", 76 | "react": "^17.0.1", 77 | "react-dom": "^17.0.1", 78 | "minimist": "^1.2.2" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /stories/1-ReactTrixWithCustomToolbar.stories.js: -------------------------------------------------------------------------------- 1 | import Trix from 'trix'; 2 | import React, { Fragment } from 'react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import { ReactTrixRTEInput, ReactTrixRTEToolbar } from '../index'; 5 | 6 | export default { 7 | title: 'React Trix Editor With Custom Toolbar', 8 | component: ReactTrixRTEToolbar, 9 | }; 10 | 11 | 12 | export const WithCustomToolbar = () => ( 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export const WithToolbarActionNotGrouped = () => ( 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | export const WithCustomizedToolbarActionWithoutGrouping = () => ( 27 | 28 | 33 | 34 | 35 | ); 36 | 37 | export const WithCustomizedToolbarActionWithGrouping = () => ( 38 | 39 | 43 | 44 | 45 | ); 46 | 47 | export const WithCustomToolbarAction = () => { 48 | const customOption = { 49 | table: { 50 | type: "button", 51 | classNames: "trix-button trix-button--icon trix-button--icon-bold", 52 | languageKey: "table", 53 | tabIndex: "-1", 54 | trixButtonGroup: "text-tools", 55 | data: { 56 | trixAttribute: "table", 57 | trixKey: "ta", 58 | }, 59 | } 60 | }; 61 | 62 | return ( 63 | 64 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | export const WithRailsDirectUpload = () => ( 74 | 75 | 76 | 77 | 78 | ); 79 | 80 | export const WithPlaceholder = () => ( 81 | 82 | 83 | 84 | 85 | ); 86 | 87 | export const WithAutoFocus = () => ( 88 | 89 | 90 | 91 | 92 | ); 93 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/ReactTrixRTEToolbar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from 'prop-types'; 3 | 4 | import ToolbarButton from "./ToolbarComponent/ToolbarButton"; 5 | import ToolbarSpacer from "./ToolbarComponent/ToolbarSpacer"; 6 | import ToolbarLinkDialog from "./ToolbarComponent/ToolbarLinkDialog"; 7 | import ToolbarButtonGroup from "./ToolbarComponent/ToolbarButtonGroup"; 8 | import { groupBy } from "../Shared/utils"; 9 | import { TOOLBAR_ACTION_OPTS, SPACER_BEFORE_TOOL_GROUP } from "./constants"; 10 | 11 | function ReactTrixRTEToolbar(props) { 12 | const { disableGroupingAction = false, toolbarId, toolbarActions, customToolbarActions } = props; 13 | const isToolbarActionPresent = toolbarActions && toolbarActions.length !== 0; 14 | let allowedToolbarActions = Object.assign(TOOLBAR_ACTION_OPTS, customToolbarActions); 15 | if(isToolbarActionPresent) { 16 | allowedToolbarActions = toolbarActions.reduce((actions, toolbarAction) => { 17 | const actionOptions = TOOLBAR_ACTION_OPTS[toolbarAction] 18 | if (actionOptions) actions[toolbarAction] = actionOptions 19 | return actions 20 | }, {}); 21 | } 22 | 23 | function renderGroupedToolbarActions() { 24 | const groupedToolbarActionOptions = groupBy(allowedToolbarActions, "trixButtonGroup"); 25 | let groupedToolbarActionHTML = []; 26 | 27 | for (let groupName in groupedToolbarActionOptions) { 28 | const toolbarActionOptions = groupedToolbarActionOptions[groupName] 29 | if (groupName === SPACER_BEFORE_TOOL_GROUP) { 30 | const dateTimestamp = new Date().getTime(); 31 | groupedToolbarActionHTML.push(); 32 | } 33 | 34 | groupedToolbarActionHTML.push( 35 | 40 | ); 41 | } 42 | 43 | return groupedToolbarActionHTML; 44 | } 45 | 46 | function renderUnGroupedToolbarActions() { 47 | const ungroupedToolbarActionsHTML = [] 48 | for (let toolbarActionKey in allowedToolbarActions) { 49 | ungroupedToolbarActionsHTML.push( 50 | 51 | ) 52 | } 53 | return ungroupedToolbarActionsHTML 54 | } 55 | 56 | function renderToolbarActions() { 57 | if(disableGroupingAction) { 58 | return( 59 | 60 | {renderUnGroupedToolbarActions()} 61 | 62 | ); 63 | } else { 64 | return renderGroupedToolbarActions(); 65 | } 66 | } 67 | 68 | function renderToolbarLinkDialog() { 69 | if (isToolbarActionPresent && toolbarActions.includes("link")) { 70 | return ; 71 | } else if(!isToolbarActionPresent) { 72 | return ; 73 | } 74 | } 75 | 76 | return ( 77 | 78 |
79 | {renderToolbarActions()} 80 |
81 | {renderToolbarLinkDialog()} 82 |
83 | ); 84 | } 85 | 86 | ReactTrixRTEToolbar.propTypes = { 87 | disableGroupingAction: PropTypes.bool, 88 | toolbarId: PropTypes.string, 89 | toolbarActions: PropTypes.array, 90 | customToolbarActions: PropTypes.object, 91 | } 92 | 93 | export default ReactTrixRTEToolbar; 94 | -------------------------------------------------------------------------------- /stories/0-ReactTrixInputRTE.stories.js: -------------------------------------------------------------------------------- 1 | import Trix from 'trix'; 2 | import React, { useRef } from 'react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import { withCssResources } from '@storybook/addon-cssresources'; 5 | import { ReactTrixRTEInput } from '../index'; 6 | 7 | export default { 8 | title: 'React Trix Editor', 9 | component: ReactTrixRTEInput, 10 | }; 11 | 12 | function ReactTrixInputRefComponent() { 13 | const reactTrixInputRef = useRef() 14 | const handleInitialize = (event) => { 15 | reactTrixInputRef.current.editor.insertHTML("Added using trixInputRef programmatically!"); 16 | } 17 | 18 | return ( 19 | 23 | ) 24 | } 25 | 26 | export const DefaultReactTrixEditor = () => ( 27 | 28 | ); 29 | 30 | export const WithDefaultValue = () => ( 31 | 34 | ); 35 | 36 | export const WithTrixInputRef = () => ( 37 | 38 | ) 39 | 40 | export const WithOnChangeHandler = () => ( 41 | 42 | ); 43 | 44 | export const WithOnBlur = () => ( 45 | 46 | ); 47 | 48 | export const WithOnFocus = () => ( 49 | 50 | ); 51 | 52 | export const WithBeforeInitialize = () => ( 53 | 54 | ); 55 | 56 | export const WithInitialize = () => ( 57 | 58 | ); 59 | 60 | export const WithSelectionChange = () => ( 61 | 62 | ); 63 | 64 | export const WithFileAccepted = () => ( 65 | 66 | ); 67 | 68 | export const WithAttachmentAdd = () => ( 69 | 70 | ); 71 | 72 | export const WithAttachmentRemove = () => ( 73 | 74 | ); 75 | 76 | export const WithIsRailsDirectUpload = () => ( 77 | 78 | ); 79 | 80 | export const WithPlaceholder = () => ( 81 | 82 | ); 83 | 84 | export const WithAutoFocus = () => ( 85 | 86 | ); 87 | 88 | export const WithCustomClassName = () => ( 89 | 90 | ); 91 | 92 | WithCustomClassName.parameters = { 93 | cssresources: [{ 94 | id: "Blue Border", 95 | class: "tx-rte-with-border", 96 | code: ``, 97 | picked: false 98 | }] 99 | } 100 | 101 | WithCustomClassName.decorators = [withCssResources] 102 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEInput/ReactTrixRTEInput.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { useState, useRef, useEffect } from "react"; 3 | import { getUniqInputId } from '../Shared/utils'; 4 | 5 | import { RAILS_DIRECT_UPLOADS_URL, RAILS_SERVICE_BLOB_URL } from "./constants"; 6 | import './ReactTrixRTEInput.style'; 7 | 8 | function ReactTrixRTEInput(props) { 9 | const { 10 | defaultValue, 11 | toolbarId, 12 | onBlur, 13 | onFocus, 14 | onChange, 15 | onInitialize, 16 | onFileAccepted, 17 | onAttachmentAdd, 18 | onAttachmentRemove, 19 | onSelectionChange, 20 | onBeforeInitialize, 21 | trixInputRef, 22 | isRailsDirectUpload, 23 | placeholder, 24 | autofocus, 25 | className, 26 | railsDirectUploadUrl, 27 | railsBlobUrl 28 | } = props; 29 | const trixRTEInputRef = trixInputRef ? trixInputRef : useRef(); 30 | const [value, setValue] = useState(defaultValue); 31 | const trixRTEInputId = props.id || getUniqInputId(); 32 | const trixRTEInputName = props.name || "content"; 33 | const directUploadOptions = isRailsDirectUpload ? { 34 | "data-direct-upload-url": railsDirectUploadUrl || RAILS_DIRECT_UPLOADS_URL, 35 | "data-blob-url-template": railsBlobUrl || RAILS_SERVICE_BLOB_URL 36 | } : {}; 37 | let trixEditorOptions = {} 38 | if (autofocus) trixEditorOptions["autofocus"] = true; 39 | if (className) trixEditorOptions["class"] = className; 40 | 41 | useEffect(() => { 42 | if (trixRTEInputRef && trixRTEInputRef.current) { 43 | trixRTEInputRef.current.addEventListener("trix-change", handleChange); 44 | if (onFocus) trixRTEInputRef.current.addEventListener("trix-focus", onFocus); 45 | if (onBlur) trixRTEInputRef.current.addEventListener("trix-blur", onBlur); 46 | if (onInitialize) trixRTEInputRef.current.addEventListener("trix-initialize", onInitialize); 47 | if (onFileAccepted) trixRTEInputRef.current.addEventListener("trix-file-accept", onFileAccepted); 48 | if (onAttachmentAdd) trixRTEInputRef.current.addEventListener("trix-attachment-add", onAttachmentAdd); 49 | if (onAttachmentRemove) trixRTEInputRef.current.addEventListener("trix-attachment-remove", onAttachmentRemove); 50 | if (onSelectionChange) trixRTEInputRef.current.addEventListener("trix-selection-change", onSelectionChange); 51 | if (onBeforeInitialize) trixRTEInputRef.current.addEventListener("trix-before-initialize", onBeforeInitialize); 52 | } 53 | 54 | return () => { 55 | if (trixRTEInputRef && trixRTEInputRef.current) { 56 | trixRTEInputRef.current.removeEventListener("trix-change", handleChange); 57 | if (onFocus) trixRTEInputRef.current.removeEventListener("trix-focus", onFocus); 58 | if (onBlur) trixRTEInputRef.current.removeEventListener("trix-blur", onBlur); 59 | if (onInitialize) trixRTEInputRef.current.removeEventListener("trix-initialize", onInitialize); 60 | if (onFileAccepted) trixRTEInputRef.current.removeEventListener("trix-file-accept", onFileAccepted); 61 | if (onAttachmentAdd) trixRTEInputRef.current.removeEventListener("trix-attachment-add", onAttachmentAdd); 62 | if (onSelectionChange) trixRTEInputRef.current.removeEventListener("trix-selection-change", onSelectionChange); 63 | if (onAttachmentRemove) trixRTEInputRef.current.removeEventListener("trix-attachment-remove", onAttachmentRemove); 64 | if (onBeforeInitialize) trixRTEInputRef.current.removeEventListener("trix-before-initialize", onBeforeInitialize); 65 | } 66 | }; 67 | }, []) 68 | 69 | function handleChange(event) { 70 | const newValue = event.target.value; 71 | setValue(newValue); 72 | if (onChange) { 73 | onChange(event, newValue); 74 | } 75 | } 76 | 77 | return ( 78 | <> 79 | 85 | 93 | 94 | ); 95 | } 96 | 97 | ReactTrixRTEInput.defaultProps = { 98 | isRailsDirectUpload: false, 99 | defaultValue: '', 100 | }; 101 | 102 | ReactTrixRTEInput.propTypes = { 103 | id: PropTypes.string, 104 | toolbarId: PropTypes.string, 105 | defaultValue: PropTypes.string, 106 | name: PropTypes.string, 107 | onChange: PropTypes.func, 108 | onBlur: PropTypes.func, 109 | onFocus: PropTypes.func, 110 | onFileAccepted: PropTypes.func, 111 | onAttachmentAdd: PropTypes.func, 112 | onAttachmentRemove: PropTypes.func, 113 | onSelectionChange: PropTypes.func, 114 | onInitialize: PropTypes.func, 115 | onBeforeInitialize: PropTypes.func, 116 | trixInputRef: PropTypes.func, 117 | isRailsDirectUpload: PropTypes.bool, 118 | placeholder: PropTypes.string, 119 | autofocus: PropTypes.bool, 120 | className: PropTypes.string, 121 | railsDirectUploadUrl: PropTypes.string, 122 | railsBlobUrl: PropTypes.string 123 | }; 124 | 125 | export default ReactTrixRTEInput; 126 | -------------------------------------------------------------------------------- /src/components/ReactTrixRTEToolbar/constants.js: -------------------------------------------------------------------------------- 1 | export const TOOLBAR_ACTION_OPTS = { 2 | bold: { 3 | type: "button", 4 | classNames: "trix-button trix-button--icon trix-button--icon-bold", 5 | languageKey: "bold", 6 | tabIndex: "-1", 7 | trixButtonGroup: "text-tools", 8 | data: { 9 | trixAttribute: "bold", 10 | trixKey: "b", 11 | }, 12 | }, 13 | italic: { 14 | type: "button", 15 | classNames: "trix-button trix-button--icon trix-button--icon-italic", 16 | languageKey: "italic", 17 | tabIndex: "-1", 18 | trixButtonGroup: "text-tools", 19 | data: { 20 | trixAttribute: "italic", 21 | trixKey: "i", 22 | }, 23 | }, 24 | strike: { 25 | type: "button", 26 | classNames: "trix-button trix-button--icon trix-button--icon-strike", 27 | languageKey: "strike", 28 | tabIndex: "-1", 29 | trixButtonGroup: "text-tools", 30 | data: { 31 | trixAttribute: "strike" 32 | }, 33 | }, 34 | link: { 35 | type: "button", 36 | classNames: "trix-button trix-button--icon trix-button--icon-link", 37 | languageKey: "link", 38 | tabIndex: "-1", 39 | trixButtonGroup: "text-tools", 40 | data: { 41 | trixAttribute: "href", 42 | trixKey: "k", 43 | trixAction: "link" 44 | }, 45 | }, 46 | heading1: { 47 | type: "button", 48 | classNames: "trix-button trix-button--icon trix-button--icon-heading-1", 49 | languageKey: "heading1", 50 | tabIndex: "-1", 51 | trixButtonGroup: "block-tools", 52 | data: { 53 | trixAttribute: "heading1" 54 | }, 55 | }, 56 | quote: { 57 | type: "button", 58 | classNames: "trix-button trix-button--icon trix-button--icon-quote", 59 | languageKey: "quote", 60 | tabIndex: "-1", 61 | trixButtonGroup: "block-tools", 62 | data: { 63 | trixAttribute: "quote" 64 | }, 65 | }, 66 | code: { 67 | type: "button", 68 | classNames: "trix-button trix-button--icon trix-button--icon-code", 69 | languageKey: "code", 70 | tabIndex: "-1", 71 | trixButtonGroup: "block-tools", 72 | data: { 73 | trixAttribute: "code" 74 | }, 75 | }, 76 | bullet: { 77 | type: "button", 78 | classNames: "trix-button trix-button--icon trix-button--icon-bullet-list", 79 | languageKey: "bullets", 80 | tabIndex: "-1", 81 | trixButtonGroup: "block-tools", 82 | data: { 83 | trixAttribute: "bullet" 84 | }, 85 | }, 86 | number: { 87 | type: "button", 88 | classNames: "trix-button trix-button--icon trix-button--icon-number-list", 89 | languageKey: "numbers", 90 | tabIndex: "-1", 91 | trixButtonGroup: "block-tools", 92 | data: { 93 | trixAttribute: "number" 94 | }, 95 | }, 96 | outdent: { 97 | type: "button", 98 | classNames: "trix-button trix-button--icon trix-button--icon-decrease-nesting-level", 99 | languageKey: "outdent", 100 | tabIndex: "-1", 101 | trixButtonGroup: "block-tools", 102 | data: { 103 | trixAction: "decreaseNestingLevel" 104 | }, 105 | }, 106 | indent: { 107 | type: "button", 108 | classNames: "trix-button trix-button--icon trix-button--icon-increase-nesting-level", 109 | languageKey: "indent", 110 | tabIndex: "-1", 111 | trixButtonGroup: "block-tools", 112 | data: { 113 | trixAction: "increaseNestingLevel" 114 | }, 115 | }, 116 | attachFiles: { 117 | type: "button", 118 | classNames: "trix-button trix-button--icon trix-button--icon-attach", 119 | languageKey: "attachFiles", 120 | tabIndex: "-1", 121 | trixButtonGroup: "file-tools", 122 | data: { 123 | trixAction: "attachFiles" 124 | }, 125 | }, 126 | undo: { 127 | type: "button", 128 | classNames: "trix-button trix-button--icon trix-button--icon-undo", 129 | languageKey: "undo", 130 | tabIndex: "-1", 131 | trixButtonGroup: "history-tools", 132 | data: { 133 | trixAction: "undo", 134 | trixKey: "z", 135 | }, 136 | }, 137 | redo: { 138 | type: "button", 139 | classNames: "trix-button trix-button--icon trix-button--icon-redo", 140 | languageKey: "redo", 141 | tabIndex: "-1", 142 | trixButtonGroup: "history-tools", 143 | data: { 144 | trixAction: "redo", 145 | trixKey: "shift+z", 146 | }, 147 | }, 148 | }; 149 | 150 | export const TOOLBAR_LANGUAGE_OPTS = { 151 | attachFiles: "Attach Files", 152 | bold: "Bold", 153 | bullets: "Bullets", 154 | byte: "Byte", 155 | bytes: "Bytes", 156 | captionPlaceholder: "Add a caption…", 157 | code: "Code", 158 | heading1: "Heading", 159 | indent: "Increase Level", 160 | italic: "Italic", 161 | link: "Link", 162 | numbers: "Numbers", 163 | outdent: "Decrease Level", 164 | quote: "Quote", 165 | redo: "Redo", 166 | remove: "Remove", 167 | strike: "Strikethrough", 168 | undo: "Undo", 169 | unlink: "Unlink", 170 | url: "URL", 171 | urlPlaceholder: "Enter a URL…", 172 | GB: "GB", 173 | KB: "KB", 174 | MB: "MB", 175 | PB: "PB", 176 | TB: "TB", 177 | }; 178 | 179 | export const TOOLBAR_ACTION_GROUP_OPTS = { 180 | "text-tools": "trix-button-group trix-button-group--text-tools", 181 | "block-tools": "trix-button-group trix-button-group--block-tools", 182 | "file-tools": "trix-button-group trix-button-group--file-tools", 183 | "history-tools": "trix-button-group trix-button-group--history-tools" 184 | }; 185 | 186 | export const SPACER_BEFORE_TOOL_GROUP = "history-tools"; 187 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React Trix RTE 2 | 3 | ![NPM](https://img.shields.io/npm/l/react-trix-rte?style=flat-square) 4 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/abhaynikam/react-trix-rte/Tests?style=flat-square) 5 | ![npm](https://img.shields.io/npm/dt/react-trix-rte?style=flat-square) 6 | 7 | React Trix rich text editor is react wrapper built for the Trix editor created by [Basecamp](https://trix-editor.org/). We have built this library because we were rewriting the same component in multiple project. 8 | 9 | This wrapper uses React hooks and adds readable event listeners on the Trix editor. The library also adds two toolbar components which has the ability to customize the toolbar actions as per our need. 10 | 11 | ### Demo 12 | 13 | Please see the some live example on [ReactTrixRTE-Storybook](https://abhaynikam.github.io/react-trix-rte) 14 | 15 | ![ReactTrixRTE](https://d1sz9tkli0lfjq.cloudfront.net/items/242v2m2W362X110O2307/Screen%20Recording%202020-04-06%20at%2012.40%20AM.gif) 16 | 17 | 18 | ### Installation 19 | 20 | To install the React Trix RTE, run the following command in the console. 21 | 22 | ``` 23 | npm install react-trix-rte 24 | OR 25 | yarn add react-trix-rte 26 | ``` 27 | 28 | ### Usage 29 | 30 | ```javascript 31 | import Trix from "trix"; 32 | import React, { useState } from "react"; 33 | import { ReactTrixRTEInput } from "react-trix-rte"; 34 | 35 | export default function TrixEditor(props) { 36 | const [value, setValue] = useState(""); 37 | 38 | function handleChange(event, newValue) { 39 | setValue(newValue); // OR custom on change listener. 40 | } 41 | 42 | return ( 43 | 47 | ) 48 | } 49 | ``` 50 | 51 | ### Upgrading to 1.0.7 or higher 52 | React Trix RTE version `1.0.7` removes the dependency import for Trix because using Trix outside directly causes problems. Read issue: [17](https://github.com/abhaynikam/react-trix-rte/issues/17) and [19](https://github.com/abhaynikam/react-trix-rte/pull/19). 53 | 54 | Import `import Trix from "trix";` to the component using React Trix RTE. 55 | 56 | ### ReactTrixInput API 57 | 58 | `ReactTrixInput` is the Trix editor input which by default comes with the toolbar. The `ReactTrixInput` comes with following properties that could be accepted. 59 | 60 | | Name | Type | Description | 61 | | ------------------- | ---- | ----------- | 62 | | id | string | The HTML id attribute for the input field 63 | | name | string | The HTML name attribute for the input field 64 | | toolbarId | string | If a custom toolbar is used for the Trix Input, pass the `toolbarId` of the custom toolbar to the input. | 65 | | isRailsDirectUpload | boolean | React Trix editor support direct uploading of the files to the service if you are using Rails as a backend server. This defaults to `false` | 66 | | railsDirectUploadUrl| string | Custom URL for Rails direct upload (`data-direct-upload-url`) | 67 | | railsBlobUrl | string | Custom URL for Rails blob template (`data-blob-url-template`) | 68 | | placeholder | string | Adds a placeholder to the React Trix Input | 69 | | defaultValue | string | The default value of the React Trix Input | 70 | | autofocus | boolean | Autofocus in the trix input. This is defaults to `false` | 71 | | className | string | Apply a custom css class to the editor | 72 | | trixInputRef | function | Adds a custom ref to the React Trix Input to programmatically edit text. Read the documentation for manual things you can perform on Trix editor [here](https://github.com/basecamp/trix#editing-text-programmatically) | 73 | | onBeforeInitialize | function | Fires when the `` element is attached to the DOM just before Trix installs its editor object. | 74 | | onInitialize | function | Fires when the `` element is attached to the DOM and its editor object is ready for use. | 75 | | onChange | function | Fires whenever the editor’s contents have changed. | 76 | | onSelectionChange | function | Fires any time the selected range changes in the editor. | 77 | | onBlur | function | Fire when the editor loses focus. | 78 | | onFocus | function | Fire when the editor gains focus. | 79 | | onFileAccepted | function | Fires when a file is dropped or inserted into the editor. You can access the DOM File object through the file property on the event. Call preventDefault on the event to prevent attaching the file to the document. | 80 | | onAttachmentAdd | function | Fires after an attachment is added to the document. You can access the Trix attachment object through the attachment property on the event. If the attachment object has a file property, you should store this file remotely and set the attachment’s URL attribute. See the attachment example for detailed information. | 81 | | onAttachmentRemove | function | Fires when an attachment is removed from the document. You can access the Trix attachment object through the attachment property on the event. You may wish to use this event to clean up remotely stored files. | 82 | 83 | ### ReactTrixRTEToolbar API 84 | 85 | `ReactTrixRTEToolbar` is the custom Trix editor toolbar component. This component helps in customizing the toolbar options, classes and attributes in better way. 86 | 87 | | Name | Type | Description | 88 | | ------------------- | ---- | ----------- | 89 | | toolbarId | string | The ReactTrixInput initialize the default toolbar if the `toolbarId` is missing or not matching. Make sure the `toolbarId` matches. | 90 | | disableGroupingAction | boolean | Defaults to `false`. If the `disableGroupingAction` is enabled the toolbar actions are not grouped for a type. Example: text tools won't be grouped | 91 | | toolbarActions | array | Allows customizing the list of toolbar actions. The available actions are `["bold", "italic", "strike", "link", "heading1", "quote", "code", "bullet", "number", "outdent", "indent", "attachFiles", "undo", "redo"]`. | 92 | | customToolbarActions | object | Override the toolbar actions properties or add custom toolbar actions. You can refer to default [toolbar actions options](https://github.com/abhaynikam/react-trix-rte/blob/master/src/components/ReactTrixRTEToolbar/constants.js) | 93 | 94 | ### Custom Toolbar Usage 95 | 96 | ```javascript 97 | import React, { useState } from "react"; 98 | import Trix from "trix"; 99 | import { ReactTrixRTEInput, ReactTrixRTEToolbar } from "react-trix-rte"; 100 | 101 | export default function TrixEditor(props) { 102 | const [value, setValue] = useState(""); 103 | 104 | function handleChange(event, newValue) { 105 | setValue(newValue); // OR custom on change listener. 106 | } 107 | 108 | return ( 109 | 110 | 111 | 116 | 117 | ) 118 | } 119 | ``` 120 | 121 | ### Contributing 122 | 123 | Read more about [contributing](https://github.com/abhaynikam/react-trix-rte/blob/7704eb681801fe2eacd00a5da50cd0bfb8c0238d/CONTRIBUTING.md) to the ReactTrixRTE. 124 | 125 | ### Author 126 | [Abhay Nikam](https://www.abhaynikam.me/pages/about) 127 | 128 | ### Contributor 129 | [CUnknown](https://github.com/CUnknown) 130 | 131 | ### License 132 | This project is licensed under the [MIT License](https://github.com/abhaynikam/react-trix-rte/blob/7704eb681801fe2eacd00a5da50cd0bfb8c0238d/LICENSE) 133 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("react")):"function"==typeof define&&define.amd?define(["react"],e):"object"==typeof exports?exports["react-trix-rte"]=e(require("react")):t["react-trix-rte"]=e(t.react)}(this,(function(t){return function(t){var e={};function r(o){if(e[o])return e[o].exports;var i=e[o]={i:o,l:!1,exports:{}};return t[o].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=t,r.c=e,r.d=function(t,e,o){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)r.d(o,i,function(e){return t[e]}.bind(null,i));return o},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=9)}([function(t,e,r){t.exports=r(2)()},function(e,r){e.exports=t},function(t,e,r){"use strict";var o=r(3);function i(){}function n(){}n.resetWarningCache=i,t.exports=function(){function t(t,e,r,i,n,a){if(a!==o){var l=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function e(){return t}t.isRequired=t;var r={array:t,bool:t,func:t,number:t,object:t,string:t,symbol:t,any:t,arrayOf:e,element:t,elementType:t,instanceOf:e,node:t,objectOf:e,oneOf:e,oneOfType:e,shape:e,exact:e,checkPropTypes:n,resetWarningCache:i};return r.PropTypes=r,r}},function(t,e,r){"use strict";t.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},function(t,e,r){var o=r(5);"string"==typeof o&&(o=[[t.i,o,""]]);var i={hmr:!0,transform:void 0,insertInto:void 0};r(7)(o,i);o.locals&&(t.exports=o.locals)},function(t,e,r){(t.exports=r(6)(!1)).push([t.i,'trix-editor{border:1px solid #bbb;border-radius:3px;margin:0;padding:0.4em 0.6em;min-height:5em;outline:none}trix-toolbar *{box-sizing:border-box}trix-toolbar .trix-button-row{display:flex;flex-wrap:nowrap;justify-content:space-between;overflow-x:auto}trix-toolbar .trix-button-group{display:flex;margin-bottom:10px;border:1px solid #bbb;border-top-color:#ccc;border-bottom-color:#888;border-radius:3px}trix-toolbar .trix-button-group:not(:first-child){margin-left:1.5vw}@media (max-device-width: 768px){trix-toolbar .trix-button-group:not(:first-child){margin-left:0}}trix-toolbar .trix-button-group-spacer{flex-grow:1}@media (max-device-width: 768px){trix-toolbar .trix-button-group-spacer{display:none}}trix-toolbar .trix-button{position:relative;float:left;color:rgba(0,0,0,0.6);font-size:0.75em;font-weight:600;white-space:nowrap;padding:0 0.5em;margin:0;outline:none;border:none;border-bottom:1px solid #ddd;border-radius:0;background:transparent}trix-toolbar .trix-button:not(:first-child){border-left:1px solid #ccc}trix-toolbar .trix-button.trix-active{background:#cbeefa;color:black}trix-toolbar .trix-button:not(:disabled){cursor:pointer}trix-toolbar .trix-button:disabled{color:rgba(0,0,0,0.125)}@media (max-device-width: 768px){trix-toolbar .trix-button{letter-spacing:-0.01em;padding:0 0.3em}}trix-toolbar .trix-button--icon{font-size:inherit;width:2.6em;height:1.6em;max-width:calc(0.8em + 4vw);text-indent:-9999px}@media (max-device-width: 768px){trix-toolbar .trix-button--icon{height:2em;max-width:calc(0.8em + 3.5vw)}}trix-toolbar .trix-button--icon::before{display:inline-block;position:absolute;top:0;right:0;bottom:0;left:0;opacity:0.6;content:"";background-position:center;background-repeat:no-repeat;background-size:contain}@media (max-device-width: 768px){trix-toolbar .trix-button--icon::before{right:6%;left:6%}}trix-toolbar .trix-button--icon.trix-active::before{opacity:1}trix-toolbar .trix-button--icon:disabled::before{opacity:0.125}trix-toolbar .trix-button--icon-attach::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M16.5%206v11.5a4%204%200%201%201-8%200V5a2.5%202.5%200%200%201%205%200v10.5a1%201%200%201%201-2%200V6H10v9.5a2.5%202.5%200%200%200%205%200V5a4%204%200%201%200-8%200v12.5a5.5%205.5%200%200%200%2011%200V6h-1.5z%22%2F%3E%3C%2Fsvg%3E);top:8%;bottom:4%}trix-toolbar .trix-button--icon-bold::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M15.6%2011.8c1-.7%201.6-1.8%201.6-2.8a4%204%200%200%200-4-4H7v14h7c2.1%200%203.7-1.7%203.7-3.8%200-1.5-.8-2.8-2.1-3.4zM10%207.5h3a1.5%201.5%200%201%201%200%203h-3v-3zm3.5%209H10v-3h3.5a1.5%201.5%200%201%201%200%203z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-italic::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M10%205v3h2.2l-3.4%208H6v3h8v-3h-2.2l3.4-8H18V5h-8z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-link::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M9.88%2013.7a4.3%204.3%200%200%201%200-6.07l3.37-3.37a4.26%204.26%200%200%201%206.07%200%204.3%204.3%200%200%201%200%206.06l-1.96%201.72a.91.91%200%201%201-1.3-1.3l1.97-1.71a2.46%202.46%200%200%200-3.48-3.48l-3.38%203.37a2.46%202.46%200%200%200%200%203.48.91.91%200%201%201-1.3%201.3z%22%2F%3E%3Cpath%20d%3D%22M4.25%2019.46a4.3%204.3%200%200%201%200-6.07l1.93-1.9a.91.91%200%201%201%201.3%201.3l-1.93%201.9a2.46%202.46%200%200%200%203.48%203.48l3.37-3.38c.96-.96.96-2.52%200-3.48a.91.91%200%201%201%201.3-1.3%204.3%204.3%200%200%201%200%206.07l-3.38%203.38a4.26%204.26%200%200%201-6.07%200z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-strike::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12.73%2014l.28.14c.26.15.45.3.57.44.12.14.18.3.18.5%200%20.3-.15.56-.44.75-.3.2-.76.3-1.39.3A13.52%2013.52%200%200%201%207%2014.95v3.37a10.64%2010.64%200%200%200%204.84.88c1.26%200%202.35-.19%203.28-.56.93-.37%201.64-.9%202.14-1.57s.74-1.45.74-2.32c0-.26-.02-.51-.06-.75h-5.21zm-5.5-4c-.08-.34-.12-.7-.12-1.1%200-1.29.52-2.3%201.58-3.02%201.05-.72%202.5-1.08%204.34-1.08%201.62%200%203.28.34%204.97%201l-1.3%202.93c-1.47-.6-2.73-.9-3.8-.9-.55%200-.96.08-1.2.26-.26.17-.38.38-.38.64%200%20.27.16.52.48.74.17.12.53.3%201.05.53H7.23zM3%2013h18v-2H3v2z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-quote::before{background-image:url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M6%2017h3l2-4V7H5v6h3zm8%200h3l2-4V7h-6v6h3z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-heading-1::before{background-image:url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12%209v3H9v7H6v-7H3V9h9zM8%204h14v3h-6v12h-3V7H8V4z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-code::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M18.2%2012L15%2015.2l1.4%201.4L21%2012l-4.6-4.6L15%208.8l3.2%203.2zM5.8%2012L9%208.8%207.6%207.4%203%2012l4.6%204.6L9%2015.2%205.8%2012z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-bullet-list::before{background-image:url(data:image/svg+xml,%3Csvg%20version%3D%221%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%204a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm0%206a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm0%206a2%202%200%201%200%200%204%202%202%200%200%200%200-4zm4%203h14v-2H8v2zm0-6h14v-2H8v2zm0-8v2h14V5H8z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-number-list::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M2%2017h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1%203h1.8L2%2013.1v.9h3v-1H3.2L5%2010.9V10H2v1zm5-6v2h14V5H7zm0%2014h14v-2H7v2zm0-6h14v-2H7v2z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-undo::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M12.5%208c-2.6%200-5%201-6.9%202.6L2%207v9h9l-3.6-3.6A8%208%200%200%201%2020%2016l2.4-.8a10.5%2010.5%200%200%200-10-7.2z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-redo::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M18.4%2010.6a10.5%2010.5%200%200%200-16.9%204.6L4%2016a8%208%200%200%201%2012.7-3.6L13%2016h9V7l-3.6%203.6z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-decrease-nesting-level::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M3%2019h19v-2H3v2zm7-6h12v-2H10v2zm-8.3-.3l2.8%202.9L6%2014.2%204%2012l2-2-1.4-1.5L1%2012l.7.7zM3%205v2h19V5H3z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-button--icon-increase-nesting-level::before{background-image:url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M3%2019h19v-2H3v2zm7-6h12v-2H10v2zm-6.9-1L1%2014.2l1.4%201.4L6%2012l-.7-.7-2.8-2.8L1%209.9%203.1%2012zM3%205v2h19V5H3z%22%2F%3E%3C%2Fsvg%3E)}trix-toolbar .trix-dialogs{position:relative}trix-toolbar .trix-dialog{position:absolute;top:0;left:0;right:0;font-size:0.75em;padding:15px 10px;background:#fff;box-shadow:0 0.3em 1em #ccc;border-top:2px solid #888;border-radius:5px;z-index:5}trix-toolbar .trix-input--dialog{font-size:inherit;font-weight:normal;padding:0.5em 0.8em;margin:0 10px 0 0;border-radius:3px;border:1px solid #bbb;background-color:#fff;box-shadow:none;outline:none;-webkit-appearance:none;-moz-appearance:none}trix-toolbar .trix-input--dialog.validate:invalid{box-shadow:#F00 0px 0px 1.5px 1px}trix-toolbar .trix-button--dialog{font-size:inherit;padding:0.5em;border-bottom:none}trix-toolbar .trix-dialog--link{max-width:600px}trix-toolbar .trix-dialog__link-fields{display:flex;align-items:baseline}trix-toolbar .trix-dialog__link-fields .trix-input{flex:1}trix-toolbar .trix-dialog__link-fields .trix-button-group{flex:0 0 content;margin:0}trix-editor [data-trix-mutable]:not(.attachment__caption-editor){-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}trix-editor [data-trix-mutable]::-moz-selection,trix-editor [data-trix-cursor-target]::-moz-selection,trix-editor [data-trix-mutable] ::-moz-selection{background:none}trix-editor [data-trix-mutable]::selection,trix-editor [data-trix-cursor-target]::selection,trix-editor [data-trix-mutable] ::selection{background:none}trix-editor [data-trix-mutable].attachment__caption-editor:focus::-moz-selection{background:highlight}trix-editor [data-trix-mutable].attachment__caption-editor:focus::selection{background:highlight}trix-editor [data-trix-mutable].attachment.attachment--file{box-shadow:0 0 0 2px highlight;border-color:transparent}trix-editor [data-trix-mutable].attachment img{box-shadow:0 0 0 2px highlight}trix-editor .attachment{position:relative}trix-editor .attachment:hover{cursor:default}trix-editor .attachment--preview .attachment__caption:hover{cursor:text}trix-editor .attachment__progress{position:absolute;z-index:1;height:20px;top:calc(50% - 10px);left:5%;width:90%;opacity:0.9;transition:opacity 200ms ease-in}trix-editor .attachment__progress[value="100"]{opacity:0}trix-editor .attachment__caption-editor{display:inline-block;width:100%;margin:0;padding:0;font-size:inherit;font-family:inherit;line-height:inherit;color:inherit;text-align:center;vertical-align:top;border:none;outline:none;-webkit-appearance:none;-moz-appearance:none}trix-editor .attachment__toolbar{position:absolute;z-index:1;top:-0.9em;left:0;width:100%;text-align:center}trix-editor .trix-button-group{display:inline-flex}trix-editor .trix-button{position:relative;float:left;color:#666;white-space:nowrap;font-size:80%;padding:0 0.8em;margin:0;outline:none;border:none;border-radius:0;background:transparent}trix-editor .trix-button:not(:first-child){border-left:1px solid #ccc}trix-editor .trix-button.trix-active{background:#cbeefa}trix-editor .trix-button:not(:disabled){cursor:pointer}trix-editor .trix-button--remove{text-indent:-9999px;display:inline-block;padding:0;outline:none;width:1.8em;height:1.8em;line-height:1.8em;border-radius:50%;background-color:#fff;border:2px solid highlight;box-shadow:1px 1px 6px rgba(0,0,0,0.25)}trix-editor .trix-button--remove::before{display:inline-block;position:absolute;top:0;right:0;bottom:0;left:0;opacity:0.7;content:"";background-image:url(data:image/svg+xml,%3Csvg%20height%3D%2224%22%20width%3D%2224%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M19%206.4L17.6%205%2012%2010.6%206.4%205%205%206.4l5.6%205.6L5%2017.6%206.4%2019l5.6-5.6%205.6%205.6%201.4-1.4-5.6-5.6z%22%2F%3E%3Cpath%20d%3D%22M0%200h24v24H0z%22%20fill%3D%22none%22%2F%3E%3C%2Fsvg%3E);background-position:center;background-repeat:no-repeat;background-size:90%}trix-editor .trix-button--remove:hover{border-color:#333}trix-editor .trix-button--remove:hover::before{opacity:1}trix-editor .attachment__metadata-container{position:relative}trix-editor .attachment__metadata{position:absolute;left:50%;top:2em;transform:translate(-50%, 0);max-width:90%;padding:0.1em 0.6em;font-size:0.8em;color:#fff;background-color:rgba(0,0,0,0.7);border-radius:3px}trix-editor .attachment__metadata .attachment__name{display:inline-block;max-width:100%;vertical-align:bottom;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}trix-editor .attachment__metadata .attachment__size{margin-left:0.2em;white-space:nowrap}.trix-content{line-height:1.5}.trix-content *{box-sizing:border-box;margin:0;padding:0}.trix-content h1{font-size:1.2em;line-height:1.2}.trix-content blockquote{border:0 solid #ccc;border-left-width:0.3em;margin-left:0.3em;padding-left:0.6em}.trix-content [dir=rtl] blockquote,.trix-content blockquote[dir=rtl]{border-width:0;border-right-width:0.3em;margin-right:0.3em;padding-right:0.6em}.trix-content li{margin-left:1em}.trix-content [dir=rtl] li{margin-right:1em}.trix-content pre{display:inline-block;width:100%;vertical-align:top;font-family:monospace;font-size:0.9em;padding:0.5em;white-space:pre;background-color:#eee;overflow-x:auto}.trix-content img{max-width:100%;height:auto}.trix-content .attachment{display:inline-block;position:relative;max-width:100%}.trix-content .attachment a{color:inherit;text-decoration:none}.trix-content .attachment a:hover,.trix-content .attachment a:visited:hover{color:inherit}.trix-content .attachment__caption{text-align:center}.trix-content .attachment__caption .attachment__name+.attachment__size::before{content:\' \\B7 \'}.trix-content .attachment--preview{width:100%;text-align:center}.trix-content .attachment--preview .attachment__caption{color:#666;font-size:0.9em;line-height:1.2}.trix-content .attachment--file{color:#333;line-height:1;margin:0 2px 2px 2px;padding:0.4em 1em;border:1px solid #bbb;border-radius:5px}.trix-content .attachment-gallery{display:flex;flex-wrap:wrap;position:relative}.trix-content .attachment-gallery .attachment{flex:1 0 33%;padding:0 0.5em;max-width:33%}.trix-content .attachment-gallery.attachment-gallery--2 .attachment,.trix-content .attachment-gallery.attachment-gallery--4 .attachment{flex-basis:50%;max-width:50%}\n',""])},function(t,e){t.exports=function(t){var e=[];return e.toString=function(){return this.map((function(e){var r=function(t,e){var r=t[1]||"",o=t[3];if(!o)return r;if(e&&"function"==typeof btoa){var i=(a=o,"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(a))))+" */"),n=o.sources.map((function(t){return"/*# sourceURL="+o.sourceRoot+t+" */"}));return[r].concat(n).concat([i]).join("\n")}var a;return[r].join("\n")}(e,t);return e[2]?"@media "+e[2]+"{"+r+"}":r})).join("")},e.i=function(t,r){"string"==typeof t&&(t=[[null,t,""]]);for(var o={},i=0;i=0&&d.splice(e,1)}function m(t){var e=document.createElement("style");if(void 0===t.attrs.type&&(t.attrs.type="text/css"),void 0===t.attrs.nonce){var o=function(){0;return r.nc}();o&&(t.attrs.nonce=o)}return f(e,t.attrs),h(t,e),e}function f(t,e){Object.keys(e).forEach((function(r){t.setAttribute(r,e[r])}))}function v(t,e){var r,o,i,n;if(e.transform&&t.css){if(!(n="function"==typeof e.transform?e.transform(t.css):e.transform.default(t.css)))return function(){};t.css=n}if(e.singleton){var a=u++;r=s||(s=m(e)),o=F.bind(null,r,a,!1),i=F.bind(null,r,a,!0)}else t.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(r=function(t){var e=document.createElement("link");return void 0===t.attrs.type&&(t.attrs.type="text/css"),t.attrs.rel="stylesheet",f(e,t.attrs),h(t,e),e}(e),o=E.bind(null,r,e),i=function(){g(r),r.href&&URL.revokeObjectURL(r.href)}):(r=m(e),o=k.bind(null,r),i=function(){g(r)});return o(t),function(e){if(e){if(e.css===t.css&&e.media===t.media&&e.sourceMap===t.sourceMap)return;o(t=e)}else i()}}t.exports=function(t,e){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(e=e||{}).attrs="object"==typeof e.attrs?e.attrs:{},e.singleton||"boolean"==typeof e.singleton||(e.singleton=a()),e.insertInto||(e.insertInto="head"),e.insertAt||(e.insertAt="bottom");var r=x(t,e);return p(r,e),function(t){for(var o=[],i=0;it.length)&&(e=t.length);for(var r=0,o=new Array(e);r0&&void 0!==arguments[0]?arguments[0]:"react-trix-rte-input-",e=(new Date).getTime(),r=Math.random().toFixed(9).slice(2);return"".concat(t).concat(e,"-").concat(r)}(),C=t.name||"content",_=m?{"data-direct-upload-url":w||"/rails/active_storage/direct_uploads","data-blob-url-template":F||"/rails/active_storage/blobs/:signed_id/*filename"}:{},O={};function j(t){var e=t.target.value;z(e),s&&s(t,e)}return v&&(O.autofocus=!0),y&&(O.class=y),Object(n.useEffect)((function(){if(k&&k.current){var t=k.current.addEventListener;t("trix-change",j),i&&t("trix-focus",i),o&&t("trix-blur",o),u&&t("trix-initialize",u),d&&t("trix-file-accept",d),b&&t("trix-attachment-add",b),p&&t("trix-attachment-remove",p),x&&t("trix-selection-change",x),h&&t("trix-before-initialize",h)}return function(){if(k&&k.current){var t=k.current.removeEventListener;t("trix-change",j),i&&t("trix-focus",i),o&&t("trix-blur",o),u&&t("trix-initialize",u),d&&t("trix-file-accept",d),b&&t("trix-attachment-add",b),x&&t("trix-selection-change",x),p&&t("trix-attachment-remove",p),h&&t("trix-before-initialize",h)}}}),[]),a.a.createElement(a.a.Fragment,null,a.a.createElement("input",{id:A,value:D,type:"hidden",name:C}),a.a.createElement("trix-editor",l({toolbar:r,placeholder:f,ref:k,input:A},_,O)))}u.defaultProps={isRailsDirectUpload:!1,defaultValue:""},u.propTypes={id:i.a.string,toolbarId:i.a.string,defaultValue:i.a.string,name:i.a.string,onChange:i.a.func,onBlur:i.a.func,onFocus:i.a.func,onFileAccepted:i.a.func,onAttachmentAdd:i.a.func,onAttachmentRemove:i.a.func,onSelectionChange:i.a.func,onInitialize:i.a.func,onBeforeInitialize:i.a.func,trixInputRef:i.a.func,isRailsDirectUpload:i.a.bool,placeholder:i.a.string,autofocus:i.a.bool,className:i.a.string,railsDirectUploadUrl:i.a.string,railsBlobUrl:i.a.string};var d=u,b={bold:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-bold",languageKey:"bold",tabIndex:"-1",trixButtonGroup:"text-tools",data:{trixAttribute:"bold",trixKey:"b"}},italic:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-italic",languageKey:"italic",tabIndex:"-1",trixButtonGroup:"text-tools",data:{trixAttribute:"italic",trixKey:"i"}},strike:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-strike",languageKey:"strike",tabIndex:"-1",trixButtonGroup:"text-tools",data:{trixAttribute:"strike"}},link:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-link",languageKey:"link",tabIndex:"-1",trixButtonGroup:"text-tools",data:{trixAttribute:"href",trixKey:"k",trixAction:"link"}},heading1:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-heading-1",languageKey:"heading1",tabIndex:"-1",trixButtonGroup:"block-tools",data:{trixAttribute:"heading1"}},quote:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-quote",languageKey:"quote",tabIndex:"-1",trixButtonGroup:"block-tools",data:{trixAttribute:"quote"}},code:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-code",languageKey:"code",tabIndex:"-1",trixButtonGroup:"block-tools",data:{trixAttribute:"code"}},bullet:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-bullet-list",languageKey:"bullets",tabIndex:"-1",trixButtonGroup:"block-tools",data:{trixAttribute:"bullet"}},number:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-number-list",languageKey:"numbers",tabIndex:"-1",trixButtonGroup:"block-tools",data:{trixAttribute:"number"}},outdent:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-decrease-nesting-level",languageKey:"outdent",tabIndex:"-1",trixButtonGroup:"block-tools",data:{trixAction:"decreaseNestingLevel"}},indent:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-increase-nesting-level",languageKey:"indent",tabIndex:"-1",trixButtonGroup:"block-tools",data:{trixAction:"increaseNestingLevel"}},attachFiles:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-attach",languageKey:"attachFiles",tabIndex:"-1",trixButtonGroup:"file-tools",data:{trixAction:"attachFiles"}},undo:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-undo",languageKey:"undo",tabIndex:"-1",trixButtonGroup:"history-tools",data:{trixAction:"undo",trixKey:"z"}},redo:{type:"button",classNames:"trix-button trix-button--icon trix-button--icon-redo",languageKey:"redo",tabIndex:"-1",trixButtonGroup:"history-tools",data:{trixAction:"redo",trixKey:"shift+z"}}},p={attachFiles:"Attach Files",bold:"Bold",bullets:"Bullets",byte:"Byte",bytes:"Bytes",captionPlaceholder:"Add a caption…",code:"Code",heading1:"Heading",indent:"Increase Level",italic:"Italic",link:"Link",numbers:"Numbers",outdent:"Decrease Level",quote:"Quote",redo:"Redo",remove:"Remove",strike:"Strikethrough",undo:"Undo",unlink:"Unlink",url:"URL",urlPlaceholder:"Enter a URL…",GB:"GB",KB:"KB",MB:"MB",PB:"PB",TB:"TB"},x={"text-tools":"trix-button-group trix-button-group--text-tools","block-tools":"trix-button-group trix-button-group--block-tools","file-tools":"trix-button-group trix-button-group--file-tools","history-tools":"trix-button-group trix-button-group--history-tools"};function h(){return(h=Object.assign||function(t){for(var e=1;e