├── src ├── webpage │ ├── static │ │ └── .gitkeep │ ├── _redirects │ └── index.html ├── test │ └── test.spec.ts └── app │ ├── components │ ├── WebGl-Display │ │ ├── WebGl-Display.css │ │ ├── shader-vertex.ts │ │ ├── shader-fragment.ts │ │ ├── Renderer.ts │ │ └── WebGl-Display.tsx │ └── Text-Editor │ │ ├── Text-Editor.css │ │ └── Text-Editor.tsx │ ├── utils │ ├── sanctuary │ │ └── Sanctuary.ts │ ├── text │ │ └── Text-Utils.ts │ ├── webgl │ │ ├── WebGl-Texture.ts │ │ └── WebGl-Shader.ts │ └── draft │ │ └── Draft-Utils.ts │ ├── AppInit.tsx │ └── App.css ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── .travis.yml ├── webpack.prod.js ├── tsconfig.json ├── wallaby.conf.js ├── README.md ├── webpack.dev.js ├── webpack.common.js ├── package.json └── .gitignore /src/webpage/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/webpage/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.minimap.enabled": false 3 | } -------------------------------------------------------------------------------- /src/test/test.spec.ts: -------------------------------------------------------------------------------- 1 | test('Tests go here', () => { 2 | expect(true).toBe(true); 3 | }); -------------------------------------------------------------------------------- /src/app/components/WebGl-Display/WebGl-Display.css: -------------------------------------------------------------------------------- 1 | .canvas { 2 | width: 50vw; 3 | height: 100vh; 4 | } -------------------------------------------------------------------------------- /src/app/components/WebGl-Display/shader-vertex.ts: -------------------------------------------------------------------------------- 1 | export const program = ` 2 | attribute vec2 a_Vertex; 3 | 4 | varying vec2 v_TexCoord; 5 | 6 | uniform mat4 u_Transform; 7 | uniform mat4 u_Size; 8 | 9 | void main() { 10 | gl_Position = u_Transform * (u_Size * vec4(a_Vertex,0,1)); 11 | v_TexCoord = a_Vertex; 12 | } 13 | `; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome", 8 | "url": "http://localhost:3000/index.html", 9 | "webRoot": "${workspaceRoot}", 10 | "sourceMaps": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /src/app/components/WebGl-Display/shader-fragment.ts: -------------------------------------------------------------------------------- 1 | export const program = ` 2 | precision mediump float; 3 | uniform vec4 u_Color; 4 | uniform sampler2D u_Sampler; 5 | varying vec2 v_TexCoord; 6 | 7 | void main() { 8 | if(u_Color.a == 0.0) { 9 | gl_FragColor = texture2D(u_Sampler, v_TexCoord); 10 | } else { 11 | gl_FragColor = u_Color; 12 | } 13 | } 14 | `; -------------------------------------------------------------------------------- /src/app/utils/sanctuary/Sanctuary.ts: -------------------------------------------------------------------------------- 1 | export interface Maybe { 2 | isNothing:boolean; 3 | isJust:boolean; 4 | value:A; 5 | } 6 | 7 | export interface Either { 8 | isLeft:boolean; 9 | isRight:boolean; 10 | value:A; 11 | } 12 | 13 | import {create, env} from 'sanctuary'; 14 | 15 | const checkTypes = false; //process.env.BUILD_TYPE !== 'build'; 16 | 17 | export const S = create({checkTypes, env}); 18 | -------------------------------------------------------------------------------- /src/app/utils/text/Text-Utils.ts: -------------------------------------------------------------------------------- 1 | //https://stackoverflow.com/questions/12006095/javascript-how-to-check-if-character-is-rtl 2 | 3 | const ltrChars = 'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF'+'\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF'; 4 | const rtlChars = '\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC'; 5 | const rtlDirCheck = new RegExp('^[^'+ltrChars+']*['+rtlChars+']'); 6 | 7 | export const stringIsRtl = s => rtlDirCheck.test(s); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm run test 11 | 12 | after_success: 13 | - npm run build 14 | 15 | deploy: 16 | local_dir: dist 17 | provider: pages 18 | skip_cleanup: true 19 | github_token: $GITHUB_TOKEN # Set in travis-ci.org dashboard 20 | on: 21 | branch: master 22 | 23 | #this fails for some reason?! 24 | #addons: 25 | #firefox: latest -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 6 | 7 | 8 | module.exports = merge(common, { 9 | 10 | devtool: "source-map", 11 | plugins: [ 12 | new UglifyJSPlugin({ 13 | sourceMap: true, 14 | }), 15 | ] 16 | }); -------------------------------------------------------------------------------- /src/webpage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Text Editor 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "baseUrl": ".", 6 | "outDir": "dist/tsc", 7 | "noImplicitAny": false, 8 | "preserveConstEnums": true, 9 | "sourceMap": true, 10 | "target": "es5", 11 | "jsx": "react", 12 | "lib": [ 13 | "es6", 14 | "es5", 15 | "dom" 16 | ] 17 | }, 18 | "include": [ 19 | "src/**/*.ts", 20 | "src/**/*.tsx" 21 | ], 22 | 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "build", 27 | "coverage" 28 | ] 29 | } -------------------------------------------------------------------------------- /wallaby.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | var path = require('path'); 3 | 4 | return { 5 | files: [ 6 | 'src/**/*.ts?(x)', 7 | 'src/**/*.snap', 8 | '!src/**/*.spec.ts?(x)' 9 | ], 10 | tests: [ 11 | 'src/**/*.spec.ts?(x)' 12 | ], 13 | 14 | env: { 15 | type: 'node', 16 | runner: 'node' 17 | }, 18 | 19 | testFramework: 'jest', 20 | 21 | setup: function (wallaby) { 22 | const jestConfig = require('./package.json').jest; 23 | jestConfig.modulePaths = jestConfig.modulePaths.map(p => p.replace('', wallaby.projectCacheDir)); 24 | wallaby.testFramework.configure(jestConfig); 25 | }, 26 | 27 | debug: true 28 | }; 29 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dakom/webgl-text-editor.svg?branch=master)](https://travis-ci.org/dakom/webgl-text-editor) 2 | 3 | # DraftJS powered WebGL Text Editor / Renderer 4 | 5 | ## [Live Demo](https://dakom.github.io/webgl-text-editor) 6 | 7 | ## Status 8 | 9 | Pretty much working now, albeit at a very basic level :) 10 | 11 | ## TODO 12 | 13 | * Size the canvas element based on the final actual dimensions 14 | * Add font-family dropdown option to menu 15 | * Add font-size dropdown option to menu 16 | * Add color picker to menu 17 | 18 | ## NOTES 19 | 20 | Canvas text can do a whole bunch of fancy things that the text editor can't show. In this demo, there could be menu options for those since there's a left/right split... but on a real app, not sure how that would be displayed. Same goes for WebGL effects. 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | console.log(process.env["NODE_ENV"]); 7 | 8 | const rules = (process.env["NODE_ENV"] === "dev-auto-reload") 9 | ? [] 10 | : [{test: path.resolve(__dirname, 'node_modules/webpack-dev-server/client'), loader: "null-loader"}] 11 | 12 | module.exports = merge(common, { 13 | devtool: "inline-source-map", 14 | module: { 15 | rules: rules 16 | }, 17 | devServer: { 18 | //contentBase: path.join(__dirname, "dist/"), 19 | contentBase: path.resolve(__dirname, './src/webpage'), 20 | compress: true, 21 | port: 4000, 22 | historyApiFallback: true, 23 | 24 | }, 25 | plugins: [ 26 | 27 | ] 28 | }); 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "showOutput": "always", 5 | "isShellCommand": true, 6 | "args": ["run"], 7 | "tasks": [ 8 | { 9 | "taskName": "Clean", 10 | "suppressTaskName": true, 11 | "isBuildCommand": false, 12 | "args": ["clean"] 13 | }, 14 | { 15 | "taskName": "Build Distrubution", 16 | "suppressTaskName": true, 17 | "isBuildCommand": true, 18 | "args": ["build:dist"], 19 | "problemMatcher": "$tsc" 20 | }, 21 | { 22 | "taskName": "Development Local Server", 23 | "suppressTaskName": true, 24 | "isBuildCommand": false, 25 | "args": ["dev"], 26 | "problemMatcher": "$tsc" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /src/app/AppInit.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import './App.css'; 5 | import {S} from "./utils/sanctuary/Sanctuary" 6 | import {TextEditor} from "./components/Text-Editor/Text-Editor"; 7 | import {WebGlDisplay} from "./components/WebGl-Display/WebGl-Display"; 8 | import {getStateFromHtml} from "./utils/draft/Draft-Utils"; 9 | import {EditorState, RichUtils} from "draft-js"; 10 | 11 | //const initialEditorState = getStateFromHtml(`Hello, בְּרֵאשִׁ֖ית`) 12 | const initialEditorState = getStateFromHtml(`Hello, בְּרֵאשִׁ֖ית`); 13 | 14 | const eventListeners = []; 15 | 16 | const MainPage = 17 | ( 18 |
19 | eventListeners.forEach(fn => fn(editorState))} 22 | eventListeners={eventListeners} 23 | /> 24 | 25 | 29 |
30 | ); 31 | 32 | ReactDOM.render(MainPage, document.getElementById('root')) 33 | -------------------------------------------------------------------------------- /src/app/utils/webgl/WebGl-Texture.ts: -------------------------------------------------------------------------------- 1 | const isPowerOf2 = (value:number):boolean => (value & (value - 1)) == 0; 2 | 3 | export interface TextureOptions { 4 | alpha: boolean; 5 | mips: boolean; 6 | } 7 | 8 | export const makeTextureFactory = (gl:WebGLRenderingContext) => (options:TextureOptions) => (target: HTMLImageElement | HTMLCanvasElement): WebGLTexture => { 9 | const {alpha, mips} = options; 10 | 11 | const format = (alpha) ? gl.RGBA : gl.RGB; 12 | const texture = gl.createTexture(); 13 | 14 | gl.bindTexture(gl.TEXTURE_2D, texture); 15 | gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 16 | 17 | 18 | //https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL 19 | if (isPowerOf2(target.width) && isPowerOf2(target.height) && mips) { 20 | gl.generateMipmap(gl.TEXTURE_2D); 21 | } else { 22 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 23 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 24 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 25 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 26 | } 27 | 28 | gl.texImage2D(gl.TEXTURE_2D, 0, format, format, gl.UNSIGNED_BYTE, target); 29 | return texture; 30 | } -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | app: path.resolve('./src/app/AppInit.tsx') 9 | }, 10 | output: { 11 | path: path.resolve("./dist"), 12 | filename: "[name].bundle.js", 13 | sourceMapFilename: "[name].bundle.map", 14 | publicPath: '' 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | { 20 | enforce: "pre", 21 | test: /\.tsx?$/, 22 | exclude: ["node_modules"], 23 | use: ["awesome-typescript-loader", "source-map-loader"] 24 | }, 25 | { test: /\.html$/, loader: "html-loader" }, 26 | { test: /\.css$/, loaders: ["style-loader", "css-loader"] }, 27 | { test: /\.glsl$/, loader: 'raw-loader'} 28 | ] 29 | }, 30 | resolve: { 31 | extensions: [".tsx", ".ts", ".js"], 32 | }, 33 | plugins: [ 34 | new CleanWebpackPlugin(['dist']), 35 | 36 | new HtmlWebpackPlugin({ 37 | template: path.resolve(__dirname, './src/webpage/index.html'), 38 | hash: true, 39 | chunks: ['app'] 40 | }), 41 | 42 | new webpack.DefinePlugin({ 43 | 'process.env': { 44 | 'NODE_ENV': JSON.stringify(process.env['NODE_ENV']) 45 | } 46 | }), 47 | ], 48 | 49 | 50 | }; -------------------------------------------------------------------------------- /src/app/components/Text-Editor/Text-Editor.css: -------------------------------------------------------------------------------- 1 | .RichEditor-root { 2 | background: #fff; 3 | border: 1px solid #ddd; 4 | font-family: 'Georgia', serif; 5 | font-size: 14px; 6 | padding: 15px; 7 | 8 | height: 50vh; 9 | width: 50vw; 10 | } 11 | 12 | .RichEditor-editor { 13 | height: 100%; 14 | border-top: 1px solid #ddd; 15 | cursor: text; 16 | font-size: 16px; 17 | margin-top: 10px; 18 | } 19 | 20 | .RichEditor-editor .public-DraftEditorPlaceholder-root, 21 | .RichEditor-editor .public-DraftEditor-content { 22 | margin: 0 -15px -15px; 23 | padding: 15px; 24 | } 25 | 26 | .RichEditor-editor .public-DraftEditor-content { 27 | min-height: 100px; 28 | } 29 | 30 | .RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root { 31 | display: none; 32 | } 33 | 34 | .RichEditor-editor .RichEditor-blockquote { 35 | border-left: 5px solid #eee; 36 | color: #666; 37 | font-family: 'Hoefler Text', 'Georgia', serif; 38 | font-style: italic; 39 | margin: 16px 0; 40 | padding: 10px 20px; 41 | } 42 | 43 | .RichEditor-editor .public-DraftStyleDefault-pre { 44 | background-color: rgba(0, 0, 0, 0.05); 45 | font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace; 46 | font-size: 16px; 47 | padding: 20px; 48 | } 49 | 50 | .RichEditor-controls { 51 | font-family: 'Helvetica', sans-serif; 52 | font-size: 14px; 53 | margin-bottom: 5px; 54 | user-select: none; 55 | } 56 | 57 | .RichEditor-styleButton { 58 | color: #999; 59 | cursor: pointer; 60 | margin-right: 16px; 61 | padding: 2px 0; 62 | display: inline-block; 63 | } 64 | 65 | .RichEditor-activeButton { 66 | color: #5890ff; 67 | } 68 | 69 | .RichEditor-controls-fontSelect, .RichEditor-controls-sizeSelect { 70 | display: block; 71 | } 72 | 73 | .RichEditor-controls-fontSelect { 74 | width: 200px; 75 | margin-right: 10px; 76 | } 77 | 78 | .RichEditor-controls-sizeSelect { 79 | width: 70px; 80 | } -------------------------------------------------------------------------------- /src/app/utils/webgl/WebGl-Shader.ts: -------------------------------------------------------------------------------- 1 | import {S,Maybe, Either} from "../sanctuary/Sanctuary"; 2 | 3 | export const CompileShader = (gl: WebGLRenderingContext) => (source:{ vertex:string; fragment:string}): WebGLProgram => { 4 | 5 | const loadSource = (gl: WebGLRenderingContext) => (shaderType: number) => (sourceText: string): Either => { 6 | const shader = gl.createShader(shaderType); 7 | gl.shaderSource(shader, sourceText); 8 | gl.compileShader(shader); 9 | 10 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 11 | const errorMessage = 'An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader); 12 | gl.deleteShader(shader); 13 | return S.Left(new Error(errorMessage)); 14 | } 15 | 16 | return S.Right(shader); 17 | } 18 | 19 | const loadShader = (shaderType: number) => (sourceText: string) => (shaderProgram:WebGLProgram) => S.chain( 20 | shader => { 21 | gl.attachShader(shaderProgram,shader); 22 | return S.Right(shaderProgram); 23 | }, 24 | loadSource(gl)(shaderType)(sourceText) 25 | ); 26 | 27 | 28 | const linkShader = (shaderProgram:WebGLProgram) => { 29 | gl.linkProgram(shaderProgram) 30 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 31 | const errorMessage = 'Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram); 32 | gl.deleteProgram(shaderProgram); 33 | return S.Left(new Error(errorMessage)); 34 | } 35 | 36 | return S.Right(shaderProgram); 37 | } 38 | 39 | 40 | const eitherShader:Either = S.pipe([ 41 | S.chain(loadShader(gl.VERTEX_SHADER) (source.vertex)), 42 | S.chain(loadShader(gl.FRAGMENT_SHADER) (source.fragment)), 43 | S.chain(linkShader), 44 | ])(S.Right(gl.createProgram())); 45 | 46 | if(eitherShader.isLeft) { 47 | //blow things up 48 | throw eitherShader.value; 49 | } else { 50 | return eitherShader.value; 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-editor", 3 | "version": "1.0.0", 4 | "description": "Text Editor", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rimraf ./dist", 8 | "test": "jest", 9 | "dev": "npm run _devserver", 10 | "dev:auto-reload": "npm run _devserver:auto-reload", 11 | "_devserver": "cross-env NODE_ENV=dev webpack-dev-server --progress --open --config webpack.dev.js", 12 | "_devserver:auto-reload": "cross-env NODE_ENV=dev-auto-reload webpack-dev-server --progress --open --config webpack.dev.js", 13 | "build": "cross-env NODE_ENV=production webpack --progress --config webpack.prod.js && cp -R ./src/webpage/static ./dist/ && cp ./src/webpage/_redirects ./dist/", 14 | "dist:server": "http-server ./dist -o" 15 | }, 16 | "author": "David Komer", 17 | "jest": { 18 | "transform": { 19 | "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" 20 | }, 21 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 22 | "moduleFileExtensions": [ 23 | "ts", 24 | "tsx", 25 | "js", 26 | "jsx", 27 | "json" 28 | ] 29 | }, 30 | "devDependencies": { 31 | "@types/chai": "4.0.10", 32 | "@types/draft-js": "0.10.19", 33 | "@types/gl-matrix": "2.4.0", 34 | "@types/jest": "22.0.0", 35 | "@types/node": "8.5.2", 36 | "@types/react": "16.0.31", 37 | "@types/react-dom": "16.0.3", 38 | "awesome-typescript-loader": "3.4.1", 39 | "chai": "4.1.2", 40 | "clean-webpack-plugin": "0.1.17", 41 | "cross-env": "5.1.3", 42 | "css-loader": "0.28.7", 43 | "draft-js": "0.10.4", 44 | "gl-matrix": "2.4.0", 45 | "html-loader": "0.5.1", 46 | "html-webpack-plugin": "2.30.1", 47 | "http-server": "0.10.0", 48 | "jest": "22.0.4", 49 | "npm-run-all": "4.1.2", 50 | "null-loader": "0.1.1", 51 | "prop-types": "15.6.0", 52 | "ramda": "0.25.0", 53 | "react": "16.2.0", 54 | "react-dom": "16.2.0", 55 | "sanctuary": "0.14.1", 56 | "source-map-loader": "0.2.3", 57 | "style-loader": "0.19.1", 58 | "ts-jest": "22.0.0", 59 | "ts-node": "4.1.0", 60 | "typescript": "2.6.2", 61 | "uglify-js": "3.3.3", 62 | "uglifyjs-webpack-plugin": "1.1.5", 63 | "webpack": "3.10.0", 64 | "webpack-dev-server": "2.9.7", 65 | "webpack-merge": "4.1.1" 66 | }, 67 | "dependencies": { 68 | "react-select": "1.1.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/components/WebGl-Display/Renderer.ts: -------------------------------------------------------------------------------- 1 | import * as R from "ramda"; 2 | import {S} from "../../utils/sanctuary/Sanctuary"; 3 | import { ContentState, convertFromHTML, convertToRaw, EditorState } from 'draft-js'; 4 | import {mat4} from "gl-matrix"; 5 | 6 | interface TextureInfo { 7 | isTexture:boolean; 8 | data?:string; 9 | isReady?:boolean; 10 | } 11 | 12 | export interface RenderData { 13 | transformMatrix:Float32Array, 14 | texture:WebGLTexture, 15 | width: number, 16 | height:number 17 | }; 18 | 19 | export const makeRenderer = gl => program => { 20 | const uSize = gl.getUniformLocation(program, "u_Size"); 21 | const uTransform = gl.getUniformLocation(program, "u_Transform"); 22 | const uColor = gl.getUniformLocation(program, "u_Color"); 23 | const uSampler = gl.getUniformLocation(program, "u_Sampler"); 24 | const aVertex = gl.getAttribLocation(program, "a_Vertex"); 25 | const sizeMatrix = mat4.create(); 26 | 27 | const vertexBufferId = gl.createBuffer(); 28 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferId); 29 | gl.bufferData( 30 | gl.ARRAY_BUFFER, 31 | Float32Array.from([ 32 | 0.0,1.0, // top-left 33 | 0.0,0.0, //bottom-left 34 | 1.0, 1.0, // top-right 35 | 1.0, 0.0 // bottom-right 36 | ]), 37 | gl.STATIC_DRAW 38 | ); 39 | 40 | return (renderData:RenderData) => { 41 | const {texture, transformMatrix, width, height} = renderData; 42 | 43 | //console.log(width, height); 44 | 45 | //Set the scaling matrix based on world dimensions 46 | mat4.fromScaling(sizeMatrix, [width, height, 1]); 47 | 48 | //Assign the uniforms 49 | gl.uniformMatrix4fv(uSize, false, sizeMatrix); 50 | gl.uniformMatrix4fv(uTransform, false, transformMatrix); 51 | 52 | //Assign the geometry 53 | gl.vertexAttribPointer(aVertex, 2, gl.FLOAT, false, 0, 0); 54 | gl.enableVertexAttribArray(aVertex); 55 | gl.bindBuffer(gl.ARRAY_BUFFER, vertexBufferId); 56 | 57 | //just make it a solid color, for testing 58 | //gl.uniform4fv(uColor, Float32Array.from([0.0, 1.0, 0.0, 1.0])); 59 | 60 | //Assign the texture 61 | gl.activeTexture(gl.TEXTURE0); 62 | gl.bindTexture(gl.TEXTURE_2D, texture); 63 | gl.uniform1i(uSampler, 0); 64 | 65 | //Render! 66 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 67 | } 68 | } -------------------------------------------------------------------------------- /src/app/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Global CSS 3 | */ 4 | 5 | 6 | /* FlexBox */ 7 | 8 | .flex, .flexFullscreenCentered { 9 | display: -webkit-box; 10 | display: -webkit-flex; 11 | display: -moz-box; 12 | display: -ms-flexbox; 13 | display: flex; 14 | } 15 | 16 | .flexColumn, .flexFullscreenCentered { 17 | -webkit-flex-direction: column; 18 | flex-direction: column; 19 | } 20 | 21 | .flexRow { 22 | -webkit-flex-direction: row; 23 | flex-direction: row; 24 | } 25 | 26 | 27 | .flexCenter, .flexFullscreenCentered { 28 | -webkit-box-align: center; 29 | -webkit-align-items: center; 30 | -moz-box-align: center; 31 | -ms-flex-align: center; 32 | align-items: center; 33 | 34 | } 35 | 36 | .flexContentCenter, .flexFullscreenCentered { 37 | -webkit-box-pack: center; 38 | -webkit-justify-content: center; 39 | -moz-box-pack: center; 40 | -ms-flex-pack: center; 41 | justify-content: center; 42 | 43 | 44 | } 45 | 46 | .flexStart { 47 | -webkit-box-align: start; 48 | -webkit-align-items: flex-start; 49 | -moz-box-align: start; 50 | -ms-flex-align: start; 51 | align-items: flex-start; 52 | } 53 | 54 | .flexEnd { 55 | -webkit-box-align: end; 56 | -webkit-align-items: flex-end; 57 | -moz-box-align: end; 58 | -ms-flex-align: end; 59 | align-items: flex-end; 60 | } 61 | 62 | .flexStretch { 63 | -webkit-box-align: stretch; 64 | -webkit-align-items: stretch; 65 | -moz-box-align: stretch; 66 | -ms-flex-align: stretch; 67 | align-items: stretch; 68 | } 69 | 70 | .flexContentEnd { 71 | -webkit-box-pack: end; 72 | -webkit-justify-content: flex-end; 73 | -moz-box-pack: end; 74 | -ms-flex-pack: end; 75 | justify-content: flex-end; 76 | } 77 | 78 | .flexContentStart { 79 | -webkit-box-pack: start; 80 | -webkit-justify-content: flex-start; 81 | -moz-box-pack: start; 82 | -ms-flex-pack: start; 83 | justify-content: flex-start; 84 | } 85 | 86 | .flexContentSpaceBetween { 87 | /* previous syntax */ 88 | -webkit-box-pack: justify; 89 | -moz-box-pack: justify; 90 | -ms-box-pack: justify; 91 | box-pack: justify; 92 | 93 | /* current syntax */ 94 | -webkit-justify-content: space-between; 95 | -moz-justify-content: space-between; 96 | -ms-justify-content: space-between; 97 | justify-content: space-between; 98 | } 99 | 100 | 101 | .flexFullscreenCentered { 102 | top: 0; 103 | right: 0; 104 | bottom: 0; 105 | left: 0; 106 | position: fixed; 107 | } -------------------------------------------------------------------------------- /src/app/utils/draft/Draft-Utils.ts: -------------------------------------------------------------------------------- 1 | import { ContentState, convertFromHTML, convertToRaw, EditorState } from 'draft-js'; 2 | import {stringIsRtl} from "../text/Text-Utils"; 3 | 4 | export const getRaw = editorState => 5 | convertToRaw(editorState.getCurrentContent()); 6 | 7 | export const getStateFromHtml = htmlString => { 8 | const blocksFromHTML = convertFromHTML(htmlString); 9 | const contentState = ContentState.createFromBlockArray( 10 | blocksFromHTML.contentBlocks, 11 | blocksFromHTML.entityMap 12 | ); 13 | 14 | blocksFromHTML.contentBlocks.forEach(block => block.getInlineStyleAt(2).forEach(style => { 15 | console.log(style); 16 | 17 | })); 18 | 19 | return EditorState.createWithContent(contentState); 20 | } 21 | 22 | export const getCanvasFromEditor = (editorState:EditorState):HTMLCanvasElement => { 23 | const canvasElement = document.createElement('canvas'); 24 | canvasElement.width = window.innerWidth/2; 25 | canvasElement.height = window.innerHeight; 26 | canvasElement.dir="rtl"; 27 | const ctx = canvasElement.getContext('2d'); 28 | const content = editorState.getCurrentContent(); 29 | 30 | let pos = { 31 | x: null, //needs to be determined based on whether text is ltr or rtl 32 | y: null //positioning this at 0 puts the text _above_ the canvas 33 | } 34 | 35 | content.getBlockMap().map(block => { 36 | const text = block.getText(); 37 | let isRtl:boolean; 38 | let textWidth:number; 39 | let fontSize = 16; 40 | 41 | 42 | block.findStyleRanges(() => true, (start, end) => { 43 | const str = text.substring(start, end); 44 | isRtl = stringIsRtl(str); 45 | 46 | let font = { 47 | style: 'normal', 48 | variant: 'normal', 49 | weight: 'normal', 50 | family: 'serif', 51 | } 52 | 53 | block.getInlineStyleAt(start).forEach(style => { 54 | if(style.indexOf("fontFamily-") === 0) { 55 | font.family = style.substring(11); //strip off the prefix 56 | } else if(style.indexOf("fontSize-") === 0) { 57 | fontSize = parseInt(style.substring(9), 10); //strip off the prefix 58 | } else { 59 | switch(style) { 60 | case "ITALIC": font.style = 'italic'; break; 61 | case "BOLD": font.weight = 'bold'; break; 62 | default: console.log(style); 63 | } 64 | } 65 | }); 66 | 67 | ctx.font = `${font.style} ${font.variant} ${font.weight} ${fontSize}px ${font.family}`; 68 | textWidth = ctx.measureText(str).width; 69 | 70 | if(pos.x === null) { 71 | //should actually be based on alignment settings... 72 | pos.x = 0; 73 | } 74 | if(pos.y === null) { 75 | pos.y = fontSize; 76 | } 77 | 78 | ctx.fillText(str, pos.x, pos.y); 79 | 80 | pos.x += isRtl ? (textWidth * -1) : textWidth; 81 | }); 82 | 83 | pos.y += fontSize * 1.2; 84 | pos.x = null; 85 | }); 86 | 87 | 88 | 89 | 90 | return canvasElement 91 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### in case we want to use temp files to not check in or large files etc. 2 | GIT-IGNORE 3 | 4 | .fusebox 5 | # production 6 | build 7 | dist 8 | test-dist 9 | test-dev 10 | coverage 11 | 12 | ### CORDOVA 13 | platforms/ 14 | plugins/ 15 | www/ 16 | 17 | # Typescript 18 | typings 19 | 20 | # misc 21 | .env 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 31 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 32 | 33 | # User-specific stuff: 34 | .idea/workspace.xml 35 | .idea/tasks.xml 36 | 37 | # Sensitive or high-churn files: 38 | .idea/dataSources/ 39 | .idea/dataSources.ids 40 | .idea/dataSources.xml 41 | .idea/dataSources.local.xml 42 | .idea/sqlDataSources.xml 43 | .idea/dynamic.xml 44 | .idea/uiDesigner.xml 45 | 46 | # Gradle: 47 | .idea/gradle.xml 48 | .idea/libraries 49 | 50 | # Mongo Explorer plugin: 51 | .idea/mongoSettings.xml 52 | 53 | ## File-based project format: 54 | *.iws 55 | 56 | ## Plugin-specific files: 57 | 58 | # IntelliJ 59 | /out/ 60 | 61 | # mpeltonen/sbt-idea plugin 62 | .idea_modules/ 63 | 64 | # JIRA plugin 65 | atlassian-ide-plugin.xml 66 | 67 | # Crashlytics plugin (for Android Studio and IntelliJ) 68 | com_crashlytics_export_strings.xml 69 | crashlytics.properties 70 | crashlytics-build.properties 71 | fabric.properties 72 | 73 | ###NODE 74 | # Logs 75 | logs 76 | *.log 77 | npm-debug.log* 78 | 79 | # Runtime data 80 | pids 81 | *.pid 82 | *.seed 83 | 84 | # Directory for instrumented libs generated by jscoverage/JSCover 85 | lib-cov 86 | 87 | # Coverage directory used by tools like istanbul 88 | coverage 89 | 90 | # nyc test coverage 91 | .nyc_output 92 | 93 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 94 | .grunt 95 | 96 | # node-waf configuration 97 | .lock-wscript 98 | 99 | # Compiled binary addons (http://nodejs.org/api/addons.html) 100 | build/Release 101 | 102 | # Dependency directories 103 | node_modules 104 | jspm_packages 105 | 106 | # Optional npm cache directory 107 | .npm 108 | 109 | # Optional REPL history 110 | .node_repl_history 111 | 112 | 113 | ###OSX 114 | 115 | .DS_Store 116 | .AppleDouble 117 | .LSOverride 118 | 119 | # Icon must end with two \r 120 | Icon 121 | 122 | 123 | # Thumbnails 124 | ._* 125 | 126 | # Files that might appear in the root of a volume 127 | .DocumentRevisions-V100 128 | .fseventsd 129 | .Spotlight-V100 130 | .TemporaryItems 131 | .Trashes 132 | .VolumeIcon.icns 133 | .com.apple.timemachine.donotpresent 134 | 135 | # Directories potentially created on remote AFP share 136 | .AppleDB 137 | .AppleDesktop 138 | Network Trash Folder 139 | Temporary Items 140 | .apdisk 141 | 142 | ###WINDOWS 143 | # Windows image file caches 144 | Thumbs.db 145 | ehthumbs.db 146 | 147 | # Folder config file 148 | Desktop.ini 149 | 150 | # Recycle Bin used on file shares 151 | $RECYCLE.BIN/ 152 | 153 | # Windows Installer files 154 | *.cab 155 | *.msi 156 | *.msm 157 | *.msp 158 | 159 | # Windows shortcuts 160 | *.lnk 161 | 162 | ###LINUX 163 | 164 | *~ 165 | 166 | # temporary files which can be created if a process still has a handle open of a deleted file 167 | .fuse_hidden* 168 | 169 | # KDE directory preferences 170 | .directory 171 | 172 | # Linux trash folder which might appear on any partition or disk 173 | .Trash-* 174 | # See http://help.github.com/ignore-files/ for more about ignoring files. 175 | 176 | # dependencies 177 | node_modules 178 | 179 | # testing 180 | coverage 181 | 182 | # production 183 | build 184 | 185 | # misc 186 | .DS_Store 187 | .env 188 | npm-debug.log 189 | -------------------------------------------------------------------------------- /src/app/components/WebGl-Display/WebGl-Display.tsx: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | import * as React from 'react'; 3 | import { ContentState, convertFromHTML, convertToRaw, EditorState } from 'draft-js'; 4 | import "./WebGl-Display.css"; 5 | import {getCanvasFromEditor} from "../../utils/draft/Draft-Utils"; 6 | import {CompileShader} from "../../utils/webgl/WebGl-Shader"; 7 | import {makeTextureFactory} from "../../utils/webgl/WebGl-Texture"; 8 | import {program as texturedQuadVertex} from './shader-vertex'; 9 | import {program as texturedQuadFragment} from './shader-fragment'; 10 | import {makeRenderer, RenderData} from "./Renderer"; 11 | import {mat4} from "gl-matrix"; 12 | 13 | //Main component 14 | interface WebGlDisplayProps { 15 | editorState: EditorState; 16 | eventListeners: Array<(editorState:EditorState) => void>; 17 | } 18 | 19 | type WebGlDisplayState = Partial; 20 | 21 | 22 | const getCanvasSize = ():{width:number, height:number} => { 23 | //might want to get these from the actual canvas element? 24 | return { 25 | width: window.innerWidth/2, 26 | height: window.innerHeight 27 | } 28 | } 29 | 30 | const getTransformMatrix = ():Float32Array => { 31 | const {width, height} = getCanvasSize(); 32 | 33 | const projection = mat4.ortho(mat4.create(), 0, width, 0, height, 0, 1) as any; 34 | const eye = mat4.create(); 35 | const camera = mat4.multiply(mat4.create(), projection, eye); 36 | 37 | const world = mat4.fromTranslation(mat4.create(), [0,0, 0]); 38 | 39 | return mat4.multiply(mat4.create(), camera, world); 40 | 41 | } 42 | 43 | 44 | export class WebGlDisplay extends React.Component { 45 | private gl:WebGLRenderingContext; 46 | private canvasElement:HTMLCanvasElement; 47 | 48 | private renderShader:(renderData:RenderData) => void; 49 | 50 | constructor(props) { 51 | super(props); 52 | 53 | this.state = {}; 54 | } 55 | 56 | componentDidMount() { 57 | //context and element setup 58 | const canvasElement = this.canvasElement; 59 | 60 | const gl = (canvasElement.getContext("webgl") as WebGLRenderingContext) || (canvasElement.getContext("experimental-webgl") as WebGLRenderingContext); 61 | this.gl = gl; 62 | 63 | //Initial WebGL setup 64 | const program = CompileShader (gl) ({ 65 | vertex: texturedQuadVertex, 66 | fragment: texturedQuadFragment 67 | }); 68 | 69 | gl.clearColor(1,0,0,1); 70 | gl.useProgram(program); 71 | this.renderShader = makeRenderer (gl) (program); 72 | const textureFactory = makeTextureFactory (gl) ({alpha: true, mips: false}); 73 | 74 | //Utility functions 75 | const createTexture = editorState => { 76 | const canvas = getCanvasFromEditor(editorState); 77 | const texture = textureFactory(canvas); 78 | 79 | 80 | return { 81 | texture: texture, 82 | width: canvas.width, 83 | height: canvas.height 84 | } 85 | } 86 | 87 | 88 | const resize = ():Float32Array => { 89 | const {width, height} = getCanvasSize(); 90 | 91 | canvasElement.setAttribute('width', width.toString()); 92 | canvasElement.setAttribute('height', height.toString()); 93 | 94 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 95 | 96 | return getTransformMatrix() 97 | } 98 | 99 | //Event handlers 100 | this.props.eventListeners.push(editorState => 101 | this.setState({...createTexture(editorState)}) 102 | ); 103 | 104 | window.addEventListener( 105 | "resize", 106 | evt => this.setState({ 107 | transformMatrix: resize() 108 | }), 109 | false 110 | ); 111 | 112 | //First draw 113 | this.setState(Object.assign({}, 114 | {...createTexture(this.props.editorState)}, 115 | { transformMatrix: resize() } 116 | )); 117 | } 118 | 119 | componentDidUpdate(prevProps, prevState) { 120 | const gl = this.gl; 121 | 122 | gl.clear(gl.COLOR_BUFFER_BIT); 123 | 124 | this.renderShader(this.state as RenderData); 125 | } 126 | 127 | render() { 128 | return this.canvasElement = el} className="canvas" /> 129 | } 130 | } -------------------------------------------------------------------------------- /src/app/components/Text-Editor/Text-Editor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Editor, EditorState, RichUtils, ContentBlock, DraftHandleValue, Modifier } from 'draft-js'; 3 | import Select from 'react-select'; 4 | import 'react-select/dist/react-select.css'; 5 | 6 | import './Text-Editor.css'; 7 | 8 | const FontList = [ 9 | "Times New Roman", 10 | "Arial", 11 | "Helvetica", 12 | "Comic Sans MS", 13 | "Impact", 14 | "Courier New", 15 | "Lucida Console", 16 | "serif", 17 | "sans-serif", 18 | "monospace" 19 | ].map(font => ({ 20 | label: font, 21 | style: `fontFamily-${font}` 22 | })); 23 | 24 | const SizeList = [ 25 | 8, 26 | 12, 27 | 16, 28 | 24, 29 | 32, 30 | 42, 31 | 50, 32 | 64, 33 | 72, 34 | 80, 35 | 92, 36 | 112, 37 | 128 38 | ].map(size => ({ 39 | label: size, 40 | style: `fontSize-${size}` 41 | })); 42 | 43 | const INLINE_STYLE_BUTTONS = [ 44 | { label: 'Bold', style: 'BOLD' }, 45 | { label: 'Italic', style: 'ITALIC' } 46 | ]; 47 | 48 | const styleMap = Object.assign({}, 49 | FontList.reduce((acc, font) => { 50 | acc[font.style] = { 51 | fontFamily: font.label 52 | } 53 | 54 | return acc; 55 | }, {}), 56 | SizeList.reduce((acc, size) => { 57 | acc[size.style] = { 58 | fontSize: size.label 59 | } 60 | 61 | return acc; 62 | }, {}) 63 | ); 64 | 65 | interface StyleButtonProps { 66 | onToggle: (style:any) => void; 67 | } 68 | class StyleButton extends React.Component { 69 | constructor(props) { 70 | super(props); 71 | this.onToggle = this.onToggle.bind(this); 72 | } 73 | 74 | onToggle(e) { 75 | e.preventDefault(); 76 | this.props.onToggle(this.props.style); 77 | }; 78 | 79 | render() { 80 | let className = 'RichEditor-styleButton'; 81 | if (this.props.active) { 82 | className += ' RichEditor-activeButton'; 83 | } 84 | 85 | return ( 86 | 87 | {this.props.label} 88 | 89 | ); 90 | } 91 | } 92 | 93 | 94 | 95 | const InlineStyleControls = (props) => { 96 | var currentStyle = props.editorState.getCurrentInlineStyle(); 97 | 98 | let selectedFont = currentStyle.find((value, key) => value.indexOf("fontFamily-") === 0); 99 | let selectedSize = currentStyle.find((value, key) => value.indexOf("fontSize-") === 0); 100 | 101 | //It seems the default draft fromHtml uses something like this... unavoidable for now 102 | if(selectedFont === undefined) { 103 | selectedFont = "fontFamily-serif"; 104 | } 105 | 106 | if(selectedSize === undefined) { 107 | selectedSize = "fontSize-16"; 108 | } 109 | 110 | return ( 111 |
112 |
113 | {INLINE_STYLE_BUTTONS.map(type => 114 | 121 | )} 122 | 123 | props.onChangeSize(selectedOption.value)} 138 | options={SizeList.map(size => ({value: size.style, label: size.label}))} 139 | /> 140 |
141 |
142 | ); 143 | }; 144 | 145 | 146 | const BlockStyleControls = (props) => { 147 | const {editorState} = props; 148 | const selection = editorState.getSelection(); 149 | const blockType = editorState 150 | .getCurrentContent() 151 | .getBlockForKey(selection.getStartKey()) 152 | .getType(); 153 | 154 | return ( 155 |
156 | 157 |
158 | ); 159 | }; 160 | 161 | //Main component 162 | interface TextEditorProps { 163 | editorState: EditorState; 164 | onEditorChanged: (editorState: EditorState) => void; 165 | eventListeners: Array<(editorState: EditorState) => void>; 166 | } 167 | 168 | export class TextEditor extends React.Component { 169 | private focus: () => void; 170 | private onChange: (editorState: EditorState) => void; 171 | 172 | private editor: Editor; 173 | constructor(props) { 174 | super(props); 175 | 176 | this.state = { 177 | editorState: props.editorState 178 | } 179 | 180 | this.props.eventListeners.push(editorState => 181 | this.setState({ 182 | editorState: editorState 183 | }) 184 | ); 185 | 186 | this.focus = () => this.editor.focus(); 187 | this.onChange = this.props.onEditorChanged; 188 | this.handleKeyCommand = this.handleKeyCommand.bind(this); 189 | this.onTab = this.onTab.bind(this); 190 | this.toggleBlockType = this.toggleBlockType.bind(this); 191 | this.toggleInlineStyle = this.toggleInlineStyle.bind(this); 192 | this.changeFont = this.changeFont.bind(this); 193 | this.changeSize = this.changeSize.bind(this); 194 | } 195 | 196 | handleKeyCommand(command, editorState):DraftHandleValue { 197 | const newState = RichUtils.handleKeyCommand(editorState, command); 198 | if (newState) { 199 | this.onChange(newState); 200 | return 'handled'; 201 | } 202 | return 'not-handled'; 203 | } 204 | 205 | onTab(e) { 206 | const maxDepth = 4; 207 | this.onChange(RichUtils.onTab(e, this.state.editorState, maxDepth)); 208 | } 209 | 210 | toggleBlockType(blockType) { 211 | console.log(blockType); 212 | 213 | this.onChange( 214 | RichUtils.toggleBlockType( 215 | this.state.editorState, 216 | blockType 217 | ) 218 | ); 219 | } 220 | 221 | toggleInlineStyle(inlineStyle) { 222 | this.onChange( 223 | RichUtils.toggleInlineStyle( 224 | this.state.editorState, 225 | inlineStyle 226 | ) 227 | ); 228 | } 229 | 230 | changeFont(value) { 231 | const {editorState} = this.state; 232 | const selection = editorState.getSelection(); 233 | const currentStyle = editorState.getCurrentInlineStyle(); 234 | 235 | const nextContentState = 236 | currentStyle.reduce((contentState, style) => 237 | (style.indexOf("fontFamily-") === 0) 238 | ? Modifier.removeInlineStyle(contentState, selection, style) 239 | : contentState 240 | , editorState.getCurrentContent()); 241 | 242 | let nextEditorState = EditorState.push( 243 | editorState, 244 | nextContentState, 245 | 'change-inline-style' 246 | ); 247 | 248 | 249 | 250 | // Unset style override... not sure what this does actually but it's from the color example 251 | if (selection.isCollapsed()) { 252 | nextEditorState = 253 | currentStyle.reduce((state, style) => { 254 | return RichUtils.toggleInlineStyle(state, style); 255 | }, nextEditorState); 256 | } 257 | 258 | // If the value exists, set it 259 | if (!currentStyle.has(value)) { 260 | nextEditorState = RichUtils.toggleInlineStyle( 261 | nextEditorState, 262 | value 263 | ); 264 | } 265 | 266 | this.onChange(nextEditorState); 267 | } 268 | 269 | changeSize(value) { 270 | const {editorState} = this.state; 271 | const selection = editorState.getSelection(); 272 | const currentStyle = editorState.getCurrentInlineStyle(); 273 | 274 | const nextContentState = 275 | currentStyle.reduce((contentState, style) => 276 | (style.indexOf("fontSize-") === 0) 277 | ? Modifier.removeInlineStyle(contentState, selection, style) 278 | : contentState 279 | , editorState.getCurrentContent()); 280 | 281 | let nextEditorState = EditorState.push( 282 | editorState, 283 | nextContentState, 284 | 'change-inline-style' 285 | ); 286 | 287 | 288 | 289 | // Unset style override... not sure what this does actually but it's from the color example 290 | if (selection.isCollapsed()) { 291 | nextEditorState = 292 | currentStyle.reduce((state, style) => { 293 | return RichUtils.toggleInlineStyle(state, style); 294 | }, nextEditorState); 295 | } 296 | 297 | // If the value exists, set it 298 | if (!currentStyle.has(value)) { 299 | nextEditorState = RichUtils.toggleInlineStyle( 300 | nextEditorState, 301 | value 302 | ); 303 | } 304 | 305 | this.onChange(nextEditorState); 306 | } 307 | 308 | render() { 309 | const { editorState } = this.state; 310 | 311 | // If the user changes block type before entering any text, we can 312 | // either style the placeholder or hide it. Let's just hide it now. 313 | let className = 'RichEditor-editor'; 314 | const contentState = editorState.getCurrentContent(); 315 | if (!contentState.hasText()) { 316 | if (contentState.getBlockMap().first().getType() !== 'unstyled') { 317 | className += ' RichEditor-hidePlaceholder'; 318 | } 319 | } 320 | 321 | return ( 322 |
323 | 329 | 333 |
334 | this.editor = editor} 342 | spellCheck={true} 343 | /> 344 |
345 |
346 | ) 347 | } 348 | } 349 | 350 | --------------------------------------------------------------------------------