├── .gitignore ├── .storybook ├── config.ts └── webpack.config.js ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── _config.yml ├── index.md └── js │ ├── .babelrc │ ├── .gitignore │ ├── demo.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── webpack.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── CanvasPane.tsx ├── Constants.ts ├── CursorPane.tsx ├── EventStore.ts ├── EventStream.ts ├── SvgConverter.ts ├── Whiteboard.tsx ├── dummy.test.ts └── index.ts ├── stories ├── Basic.tsx ├── Image.tsx ├── Layer.tsx └── UndoRedo.tsx ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .nyc_output 3 | coverage 4 | dist 5 | node_modules 6 | 7 | *.log 8 | -------------------------------------------------------------------------------- /.storybook/config.ts: -------------------------------------------------------------------------------- 1 | import {configure} from '@storybook/react' 2 | 3 | const req = require.context('../stories', true, /\.tsx$/) 4 | 5 | function loadStories() { 6 | req.keys().forEach(req) 7 | } 8 | 9 | configure(loadStories, module) 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [{ 4 | test: /\.tsx?$/, 5 | use: { 6 | loader: 'ts-loader' 7 | } 8 | }] 9 | }, 10 | resolve: { 11 | extensions: [ '.tsx', '.ts', '.js' ] 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "10" 5 | - "8" 6 | script: 7 | - npm run build 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "typescript.validate.enable": false 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | ### Changed 10 | 11 | - Migrate `typescript` from `javascript` 12 | 13 | ## 0.1.2 (2019-03-26) 14 | 15 | ### Changed 16 | 17 | - Upgrade `react` packages from `16.5.4` to `16.8.5` 18 | 19 | ## 0.1.1 (2017-10-06) 20 | 21 | ### Added 22 | 23 | - Generate source map 24 | 25 | ## 0.1.0 (2017-08-06) 26 | 27 | Initial release 28 | 29 | ### Added 30 | 31 | - Add Fundamental features 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Kenichi Ohtomi 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-whiteboard 2 | 3 | [![npm](https://img.shields.io/npm/v/@ohtomi/react-whiteboard.svg)](https://www.npmjs.com/package/@ohtomi/react-whiteboard) 4 | [![License](https://img.shields.io/npm/l/@ohtomi/react-whiteboard.svg)](LICENSE) 5 | [![Build Status](https://travis-ci.org/ohtomi/react-whiteboard.svg?branch=master)](https://travis-ci.org/ohtomi/react-whiteboard) 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/ohtomi/react-whiteboard.svg)](https://greenkeeper.io/) 7 | 8 | ## Description 9 | 10 | A whiteboard `React` component using `SVG`. 11 | 12 | ## How to use 13 | 14 | ```javascript 15 | render() { 16 | return ( 17 | 22 | ); 23 | } 24 | ``` 25 | 26 | See [examples](stories). 27 | 28 | ## How to build 29 | 30 | ```bash 31 | $ npm install 32 | $ npm run build 33 | ``` 34 | 35 | ## Contributing 36 | 37 | 1. Fork it! 38 | 1. Create your feature branch: `git checkout -b my-new-feature` 39 | 1. Commit your changes: `git commit -am 'Add some feature'` 40 | 1. Push to the branch: `git push origin my-new-feature` 41 | 1. Submit a pull request :D 42 | 43 | ## License 44 | 45 | MIT 46 | 47 | ## Author 48 | 49 | [Kenichi Ohtomi](https://github.com/ohtomi) 50 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | - To draw a line, click the `whiteboard` and move the mouse cursor in the `whiteboard`. 4 | - To change line width and line color, select choices. 5 | - To paste an image on the `whiteboard`, type an URL of the image in the text box and press the paste button. 6 | 7 |
8 |
9 | 23 |  |  24 | 33 |  |  34 | 39 |
40 | 41 | 42 | ## source code 43 | 44 | ```javascript 45 | import React from 'react'; 46 | 47 | import {Whiteboard, EventStream, EventStore} from '@ohtomi/react-whiteboard'; 48 | 49 | 50 | class Demo extends React.Component { 51 | 52 | constructor(props) { 53 | super(props); 54 | 55 | this.events = new EventStream(); 56 | this.eventStore = new EventStore(); 57 | this.width = 450; 58 | this.height = 400; 59 | this.style = { 60 | backgroundColor: 'lightyellow' 61 | }; 62 | } 63 | 64 | render() { 65 | return ( 66 | 68 | ); 69 | } 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/js/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-export-default-from", 8 | "@babel/plugin-proposal-export-namespace-from" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /docs/js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | *.log 4 | -------------------------------------------------------------------------------- /docs/js/demo.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=4)}([function(e,t,n){var r;window,e.exports=(r=n(10),function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}([function(e,t){e.exports=r},function(e,t,n){"use strict";var r,o="object"==typeof Reflect?Reflect:null,i=o&&"function"==typeof o.apply?o.apply:function(e,t,n){return Function.prototype.apply.call(e,t,n)};r=o&&"function"==typeof o.ownKeys?o.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var a=Number.isNaN||function(e){return e!=e};function l(){l.init.call(this)}e.exports=l,l.EventEmitter=l,l.prototype._events=void 0,l.prototype._eventsCount=0,l.prototype._maxListeners=void 0;var u=10;function s(e){return void 0===e._maxListeners?l.defaultMaxListeners:e._maxListeners}function c(e,t,n,r){var o,i,a,l;if("function"!=typeof n)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof n);if(void 0===(i=e._events)?(i=e._events=Object.create(null),e._eventsCount=0):(void 0!==i.newListener&&(e.emit("newListener",t,n.listener?n.listener:n),i=e._events),a=i[t]),void 0===a)a=i[t]=n,++e._eventsCount;else if("function"==typeof a?a=i[t]=r?[n,a]:[a,n]:r?a.unshift(n):a.push(n),(o=s(e))>0&&a.length>o&&!a.warned){a.warned=!0;var u=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");u.name="MaxListenersExceededWarning",u.emitter=e,u.type=t,u.count=a.length,l=u,console&&console.warn&&console.warn(l)}return e}function f(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},o=function(){for(var e=[],t=0;t0&&(a=t[0]),a instanceof Error)throw a;var l=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw l.context=a,l}var u=o[e];if(void 0===u)return!1;if("function"==typeof u)i(u,this,t);else{var s=u.length,c=h(u,s);for(n=0;n=0;i--)if(n[i]===t||n[i].listener===t){a=n[i].listener,o=i;break}if(o<0)return this;0===o?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},l.prototype.listeners=function(e){return d(this,e,!0)},l.prototype.rawListeners=function(e){return d(this,e,!1)},l.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):p.call(e,t)},l.prototype.listenerCount=p,l.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){"use strict";n.r(t);var r={};n.r(r),n.d(r,"MODE",function(){return o}),n.d(r,"SVG_ELEMENT_TYPE",function(){return i});var o={HAND:"HAND",DRAW_LINE:"DRAW_LINE",DRAG_IMAGE:"DRAG_IMAGE",NW_RESIZE_IMAGE:"NW_RESIZE_IMAGE",NE_RESIZE_IMAGE:"NE_RESIZE_IMAGE",SE_RESIZE_IMAGE:"SE_RESIZE_IMAGE",SW_RESIZE_IMAGE:"SW_RESIZE_IMAGE"},i={LINE:"LINE",IMAGE:"IMAGE"},a=n(0),l=n.n(a),u=n(1),s=n.n(u);function c(e,t){for(var n=0;n1:(e.type,i.IMAGE,!0)})}},{key:"selectLayer",value:function(e){this.goodEvents.push({}),this.selectedLayer=e}},{key:"addLayer",value:function(){this.renderableLayers.push(!0)}},{key:"startDrawing",value:function(e,t,n){this.goodEvents.push({type:i.LINE,layer:this.selectedLayer,strokeWidth:e,strokeColor:t,point:n})}},{key:"stopDrawing",value:function(){this.goodEvents.push({})}},{key:"pushPoint",value:function(e,t,n){var r={type:i.LINE,layer:this.selectedLayer,strokeWidth:e,strokeColor:t,point:n};this.goodEvents.push(r),this.undoEvents=[]}},{key:"pasteImage",value:function(e){var t={type:i.IMAGE,layer:this.selectedLayer,image:e};this.goodEvents.push(t),this.undoEvents=[]}},{key:"dragImage",value:function(e){var t=this.lastImage();t&&(t.x=t.x+e.x,t.y=t.y+e.y)}},{key:"nwResizeImage",value:function(e){var t=this.lastImage();t&&(t.x=t.x+e.x,t.y=t.y+e.y,t.width=t.width-e.x,t.height=t.height-e.y)}},{key:"neResizeImage",value:function(e){var t=this.lastImage();t&&(t.y=t.y+e.y,t.width=t.width+e.x,t.height=t.height-e.y)}},{key:"seResizeImage",value:function(e){var t=this.lastImage();t&&(t.width=t.width+e.x,t.height=t.height+e.y)}},{key:"swResizeImage",value:function(e){var t=this.lastImage();t&&(t.x=t.x+e.x,t.width=t.width-e.x,t.height=t.height+e.y)}},{key:"undo",value:function(){this.goodEvents.length&&(this.undoEvents.push(this.goodEvents.pop()),this.undoEvents.push(this.goodEvents.pop()),this.goodEvents.push({}))}},{key:"redo",value:function(){this.undoEvents.length&&(this.goodEvents.pop(),this.goodEvents.push(this.undoEvents.pop()),this.goodEvents.push(this.undoEvents.pop()))}},{key:"clear",value:function(){this.goodEvents=[],this.undoEvents=[]}}])&&d(t.prototype,n),e}();function h(e){return(h="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function m(e){return function(e){if(Array.isArray(e)){for(var t=0,n=new Array(e.length);tt?t:n},a=i(0,this.props.height,t.y+r),u=i(0,this.props.height,t.y+t.height-r),s=i(0,this.props.width,t.x+r),c=i(0,this.props.width,t.x+t.width-r),f={position:"absolute",zIndex:2500,top:a,left:s,width:c-s,height:u-a,cursor:"move"},d={position:"absolute",zIndex:2500,top:i(0,this.props.height,a-r),left:i(0,this.props.width,s-r),width:s-i(0,this.props.width,s-r),height:a-i(0,this.props.height,a-r),cursor:"nw-resize"},p={position:"absolute",zIndex:2500,top:i(0,this.props.height,a-r),left:c,width:i(0,this.props.width,c+r)-c,height:a-i(0,this.props.height,a-r),cursor:"ne-resize"},h={position:"absolute",zIndex:2500,top:u,left:c,width:i(0,this.props.width,c+r)-c,height:i(0,this.props.height,u+r)-u,cursor:"se-resize"},m={position:"absolute",zIndex:2500,top:u,left:i(0,this.props.width,s-r),width:s-i(0,this.props.width,s-r),height:i(0,this.props.height,u+r)-u,cursor:"sw-resize"};return[l.a.createElement("div",{key:"drag",role:"presentation",style:f,ref:function(t){return e.dragHandle=t},onClick:this.onClickDragHandle.bind(this)}),l.a.createElement("div",{key:"nw-resize",role:"presentation",style:d,onClick:this.onClickResizeHandle.bind(this,o.NW_RESIZE_IMAGE)}),l.a.createElement("div",{key:"ne-resize",role:"presentation",style:p,onClick:this.onClickResizeHandle.bind(this,o.NE_RESIZE_IMAGE)}),l.a.createElement("div",{key:"se-resize",role:"presentation",style:h,onClick:this.onClickResizeHandle.bind(this,o.SE_RESIZE_IMAGE)}),l.a.createElement("div",{key:"sw-resize",role:"presentation",style:m,onClick:this.onClickResizeHandle.bind(this,o.SW_RESIZE_IMAGE)})]}}])&&v(n.prototype,r),t}();function k(e){return(k="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function w(e,t){for(var n=0;nR.length&&R.push(e)}function L(e,t,n){return null==e?0:function e(t,n,r,o){var l=typeof t;"undefined"!==l&&"boolean"!==l||(t=null);var u=!1;if(null===t)u=!0;else switch(l){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case i:case a:u=!0}}if(u)return r(o,t,""===n?"."+D(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var s=0;sthis.eventPool.length&&this.eventPool.push(e)}function fe(e){e.eventPool=[],e.getPooled=se,e.release=ce}o(ue.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=ae)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=ae)},persist:function(){this.isPersistent=ae},isPersistent:le,destructor:function(){var e,t=this.constructor.Interface;for(e in t)this[e]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null,this.isPropagationStopped=this.isDefaultPrevented=le,this._dispatchInstances=this._dispatchListeners=null}}),ue.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null},ue.extend=function(e){function t(){}function n(){return r.apply(this,arguments)}var r=this;t.prototype=r.prototype;var i=new t;return o(i,n.prototype),n.prototype=i,n.prototype.constructor=n,n.Interface=o({},r.Interface,e),n.extend=r.extend,fe(n),n},fe(ue);var de=ue.extend({data:null}),pe=ue.extend({data:null}),he=[9,13,27,32],me=B&&"CompositionEvent"in window,ve=null;B&&"documentMode"in document&&(ve=document.documentMode);var ye=B&&"TextEvent"in window&&!ve,ge=B&&(!me||ve&&8=ve),be=String.fromCharCode(32),ke={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},we=!1;function Ee(e,t){switch(e){case"keyup":return-1!==he.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"blur":return!0;default:return!1}}function Se(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var xe=!1;var _e={eventTypes:ke,extractEvents:function(e,t,n,r){var o=void 0,i=void 0;if(me)e:{switch(e){case"compositionstart":o=ke.compositionStart;break e;case"compositionend":o=ke.compositionEnd;break e;case"compositionupdate":o=ke.compositionUpdate;break e}o=void 0}else xe?Ee(e,n)&&(o=ke.compositionEnd):"keydown"===e&&229===n.keyCode&&(o=ke.compositionStart);return o?(ge&&"ko"!==n.locale&&(xe||o!==ke.compositionStart?o===ke.compositionEnd&&xe&&(i=ie()):(re="value"in(ne=r)?ne.value:ne.textContent,xe=!0)),o=de.getPooled(o,t,n,r),i?o.data=i:null!==(i=Se(n))&&(o.data=i),$(o),i=o):i=null,(e=ye?function(e,t){switch(e){case"compositionend":return Se(t);case"keypress":return 32!==t.which?null:(we=!0,be);case"textInput":return(e=t.data)===be&&we?null:e;default:return null}}(e,n):function(e,t){if(xe)return"compositionend"===e||!me&&Ee(e,t)?(e=ie(),oe=re=ne=null,xe=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1