├── .bowerrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── dist ├── react-designer-core.js └── react-designer-core.min.js ├── example └── src │ ├── .gitignore │ ├── RichEditor.css │ ├── components │ ├── Designer.js │ ├── Toolbar.js │ ├── WidgetRenderer.js │ ├── Widgets.js │ └── containerMetadata.js │ ├── example.css │ ├── example.js │ ├── example.less │ ├── grid.png │ ├── index.html │ ├── react-flexr.css │ └── widgets │ ├── BackgroundContainer.js │ ├── BoxAlignmentStyle.js │ ├── CellWrapper.js │ ├── GridContainer.js │ ├── GridItem.js │ ├── GridWrapper.js │ ├── RichTextEditor.js │ ├── RichTextEditorToolbox.js │ ├── RichTextRenderer.js │ ├── TextRenderer.js │ └── utils │ ├── backgroundStyle.js │ ├── border.js │ └── font.js ├── gulpfile.js ├── lib ├── Designer.js ├── components │ ├── ObjectBrowser.js │ └── Workplace.js ├── util │ ├── ItemTypes.js │ ├── backgroundStyle.js │ └── generateCssTransform.js └── workplace │ ├── Box.js │ ├── Container.js │ ├── ResizableHandle.js │ └── ResizeContainer.js ├── package.json └── src ├── Designer.js ├── components ├── ObjectBrowser.js └── Workplace.js ├── util ├── ItemTypes.js ├── backgroundStyle.js └── generateCssTransform.js └── workplace ├── Box.js ├── Container.js ├── ResizableHandle.js └── ResizeContainer.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "example/dist/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | indent_style = tab 11 | 12 | [*.json] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .publish/* 2 | dist/* 3 | example/dist/* 4 | lib/* 5 | node_modules/* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "curly": [2, "multi-line"], 12 | "quotes": [2, "single", "avoid-escape"], 13 | "react/display-name": 0, 14 | "react/jsx-boolean-value": 1, 15 | "react/jsx-quotes": 1, 16 | "react/jsx-no-undef": 1, 17 | "react/jsx-sort-props": 0, 18 | "react/jsx-sort-prop-types": 1, 19 | "react/jsx-uses-react": 1, 20 | "react/jsx-uses-vars": 1, 21 | "react/no-did-mount-set-state": 1, 22 | "react/no-did-update-set-state": 1, 23 | "react/no-multi-comp": 1, 24 | "react/no-unknown-property": 1, 25 | "react/prop-types": 1, 26 | "react/react-in-jsx-scope": 1, 27 | "react/self-closing-comp": 1, 28 | "react/wrap-multilines": 1, 29 | "semi": 2, 30 | "strict": 0 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage tools 11 | lib-cov 12 | coverage 13 | coverage.html 14 | .cover* 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Example build directory 20 | example/dist 21 | .publish 22 | 23 | # Editor and other tmp files 24 | *.swp 25 | *.un~ 26 | *.iml 27 | *.ipr 28 | *.iws 29 | *.sublime-* 30 | .idea/ 31 | *.DS_Store 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Roman Samec (roman.samec2@gmail.com) 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-designer-core 2 | ======================= 3 | 4 | React-designer-core is a set of core components for [react-designer](https://github.com/rsamec/react-designer). This is WYSIWYG editor for **easy content creation** (legal contracts, business forms, marketing leaflets, technical guides, visual reports, rich dashboards, tutorials and other content, etc.). 5 | 6 | Live demos 7 | 8 | + [simple](http://rsamec.github.io/react-designer-core/) 9 | + [react-designer](http://rsamec.github.io/react-designer/) 10 | 11 | ## Main goals 12 | 13 | + it is for developers - to use it in your own solutions with your own widgets (components) 14 | + it is based on simple principals 15 | + minimal JSON definition - [PTT](https://github.com/rsamec/ptt) - maps directly to React components 16 | + good performance even for big documents - due to usage of immutable structure using [freezer-js](https://github.com/arqex/freezer) 17 | + simple extensible by custom components or frameworks (react-bootstrap, material-design, your company made widgets, etc.) 18 | + offers standard design components (Workplace, Container, Box, ObjectBrowser) and features 19 | + layouting - moving positions, resizing, moving up/down in component hieararchy, copying, deleting, etc. 20 | + component editing - property editors, inline editing - using [draftjs](https://facebook.github.io/draft-js/), streching, rotating, transforming [transhand](https://github.com/azazdeaz/transhand),etc. 21 | + workplace - selecting, undo/redo, previewing, import/export, etc. 22 | 23 | It comes with core components typical for WYSIWYG designers 24 | 25 | + Workplace - main working area for drawing documents 26 | + ObjectTree - logical component tree - enables to search and move components between nodes 27 | 28 | Document definition is done in JSON - uses [PTT](https://github.com/rsamec/ptt) 29 | 30 | + document definition - simple JSON - [Page Transform Tree (PTT)](https://github.com/rsamec/ptt) - it is framework agnostic definition 31 | + document rendering - visual component tree - [React virtual DOM](http://facebook.github.io/react) - rendering to DOM so that it maps each component (terminal node) from logical tree to react component and its properties 32 | 33 | 34 | ### Document definition - [PTT](https://github.com/rsamec/ptt) 35 | 36 | The PPT format is __framework agnostic__ document description. It enables to dynamically render pages in different sizes (A4,A3,Letter,...),in various formats (html,pdf,...) and for various visual media (screen, papers). 37 | 38 | It is a simple component tree that consists of these two nodes 39 | 40 | + **containers** - nodes that are containers for other components - visual and logical grouping of parts of document (sections, containers,grids, rows, cells, panels, etc. ) 41 | + **boxes** - terminal nodes (leaf) that are visible components - (components, boxes, widgets) - renders to document (typically by simple mapings to props of component) 42 | 43 | There is an minimal 'Hello world' example. The PTT consists of one container and one box with TextBox element. 44 | 45 | ```json 46 | { 47 | "name": "Hello World Example", 48 | "elementName": "PTTv1", 49 | "containers": [ 50 | { 51 | "name": "My first container", 52 | "elementName": "Container", 53 | "style": { "top": 0, "left": 0, "height": 200, "width": 740, "position": "relative" }, 54 | "boxes": [{ 55 | "name": "My first text", 56 | "elementName": "TextContent", 57 | "style": { "top": 0, "left": 0 }, 58 | "props":{ 59 | "content": "Hello world" 60 | } 61 | }] 62 | }] 63 | } 64 | ``` 65 | 66 | See the full [PTT specification](https://github.com/rsamec/ptt). 67 | 68 | ### Document rendering - visual component tree - PTT rendering in react 69 | 70 | To render react components specifies in react is really simple 71 | 72 | ```js 73 | render:{ 74 | {this.props.boxes.map(function (box, i) { 75 | var component = React.createElement(widget,box, box.content!== undefined?React.DOM.span(null, box.content):undefined); 76 | return ( 77 |
78 | {component} 79 |
80 | ); 81 | }, this)} 82 | } 83 | ``` 84 | 85 | ## Demo & Examples 86 | 87 | + [Minimal](http://rsamec.github.io/react-designer-core/) 88 | + [react-designer](http://rsamec.github.io/react-designer/) 89 | 90 | To build the examples locally, run: 91 | 92 | ``` 93 | npm install 94 | gulp dev 95 | ``` 96 | 97 | Then open [`localhost:8000`](http://localhost:8000) in a browser. 98 | 99 | 100 | ## Installation 101 | 102 | The easiest way to use this component is to install it from NPM and include it in your own React build process (using [Browserify](http://browserify.org), [Webpack](http://webpack.github.io/), etc). 103 | 104 | You can also use the standalone build by including `dist/react-designer-core.js` in your page. If you use this, make sure you have already included React, and it is available as a global variable. 105 | 106 | ``` 107 | npm install react-designer-core --save 108 | ``` 109 | 110 | ## Usage 111 | 112 | import {Workplace,ObjectBrowser} from 'react-designer-core'; 113 | 114 | ```js 115 | 116 | import React from 'react'; 117 | import Freezer from 'freezer-js'; 118 | import Designer from './Designer'; 119 | 120 | // Create a Freezer store 121 | var freezer = new Freezer({ 122 | schema: { 123 | elementName: 'ObjectSchema', 124 | name: 'New Document', 125 | containers: [] 126 | } 127 | }) 128 | 129 | export default class AppContainer extends React.Component { 130 | 131 | componentDidMount() { 132 | var me = this; 133 | 134 | // 2. Your app get re-rendered on any state change 135 | freezer.on('update', function () { 136 | me.forceUpdate() 137 | }); 138 | } 139 | 140 | render() { 141 | // 1. Your app receives the state 142 | var state = freezer.get(); 143 | return ; 144 | } 145 | } 146 | ``` 147 | 148 | ```js 149 | 150 | import React from 'react'; 151 | import {Workplace,ObjectBrowser} from 'react-designer-core'; 152 | 153 | import Widgets from './Widgets'; 154 | import WidgetRenderer from './WidgetRenderer'; 155 | 156 | 157 | export default class Designer extends React.Component { 158 | constructor(props) { 159 | super(props); 160 | this.state = { 161 | current: { 162 | node: props.state.schema 163 | }, 164 | snapGrid: [10, 10] 165 | } 166 | } 167 | 168 | currentChanged(currentNode, path) { 169 | if (currentNode === undefined) return; 170 | 171 | this.setState({ 172 | current: { 173 | node: currentNode, 174 | path: path === undefined ? this.state.current && this.state.current.path : path 175 | } 176 | } 177 | ); 178 | } 179 | 180 | render() { 181 | var schema = this.props.state.schema; 182 | return 183 |
184 | 187 | 189 |
190 | } 191 | } 192 | ``` 193 | 194 | See the example folder to see more features. 195 | 196 | ### License 197 | 198 | MIT. Copyright (c) 2015 Roman Samec 199 | 200 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-designer-core", 3 | "version": "1.3.1", 4 | "description": "Designer", 5 | "main": "dist/react-designer-core.min.js", 6 | "homepage": "https://github.com/rsamec/react-designer-core", 7 | "authors": [ 8 | "Roman Samec" 9 | ], 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "react", 17 | "react-component" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | ".editorconfig", 22 | ".gitignore", 23 | "package.json", 24 | "src", 25 | "node_modules", 26 | "example", 27 | "test" 28 | ], 29 | "dependencies": { 30 | "tinymce": "~4.1.9" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/src/.gitignore: -------------------------------------------------------------------------------- 1 | ## This file is here to ensure it is included in the gh-pages branch, 2 | ## when `gulp deploy` is used to push updates to the demo site. 3 | 4 | # Dependency directory 5 | node_modules 6 | -------------------------------------------------------------------------------- /example/src/RichEditor.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 | 9 | /*.RichEditor-editor {*/ 10 | /*border-top: 1px solid #ddd;*/ 11 | /*cursor: text;*/ 12 | /*font-size: 16px;*/ 13 | /*margin-top: 10px;*/ 14 | /*}*/ 15 | 16 | /*.RichEditor-editor .public-DraftEditorPlaceholder-root,*/ 17 | /*.RichEditor-editor .public-DraftEditor-content {*/ 18 | /*margin: 0 -15px -15px;*/ 19 | /*padding: 15px;*/ 20 | /*}*/ 21 | 22 | /*.RichEditor-editor .public-DraftEditor-content {*/ 23 | /*min-height: 100px;*/ 24 | /*}*/ 25 | 26 | /*.RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root {*/ 27 | /*display: none;*/ 28 | /*}*/ 29 | 30 | .RichEditor-editor .RichEditor-blockquote { 31 | border-left: 5px solid #eee; 32 | color: #666; 33 | font-family: 'Hoefler Text', 'Georgia', serif; 34 | font-style: italic; 35 | margin: 16px 0; 36 | padding: 10px 20px; 37 | } 38 | 39 | .RichEditor-editor .public-DraftStyleDefault-pre { 40 | background-color: rgba(0, 0, 0, 0.05); 41 | font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace; 42 | font-size: 16px; 43 | padding: 20px; 44 | } 45 | 46 | .RichEditor-controls { 47 | font-family: 'Helvetica', sans-serif; 48 | font-size: 14px; 49 | margin-bottom: 5px; 50 | user-select: none; 51 | } 52 | 53 | .RichEditor-styleButton { 54 | color: #999; 55 | cursor: pointer; 56 | margin-right: 16px; 57 | padding: 2px 0; 58 | } 59 | 60 | .RichEditor-activeButton { 61 | color: #5890ff; 62 | } 63 | -------------------------------------------------------------------------------- /example/src/components/Designer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from "lodash"; 3 | 4 | import {Workplace, ObjectBrowser} from 'react-designer-core'; 5 | import SplitPane from 'react-split-pane'; 6 | import Widgets from './Widgets'; 7 | import WidgetRenderer from './WidgetRenderer'; 8 | import Toolbar from './Toolbar'; 9 | 10 | //import { JsonTree, ADD_DELTA_TYPE, REMOVE_DELTA_TYPE, UPDATE_DELTA_TYPE } from 'react-editable-json-tree'; 11 | import getContainerProps from './containerMetadata'; 12 | 13 | export default class Designer extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | current: { 18 | node: props.state.schema 19 | }, 20 | snapGrid: [10, 10] 21 | } 22 | } 23 | 24 | currentChanged(currentNode, path) { 25 | if (currentNode === undefined) return; 26 | var parent = currentNode.__.parents; 27 | var parentNode = parent.length !== 0 ? parent[0].__.parents[0] : undefined; 28 | this.setState({ 29 | current: { 30 | node: currentNode, 31 | parentNode: parentNode, 32 | path: path === undefined ? this.state.current && this.state.current.path : path 33 | } 34 | } 35 | ); 36 | } 37 | widgetPropsChanged(updatedValue) { 38 | console.log(updatedValue); 39 | var current = this.state.current.node; 40 | 41 | //var updated = current.set({'props': updatedValue.props, 'bindings':updatedValue.binding}); 42 | var updated = current.set({'props': updatedValue}); 43 | this.currentChanged(updated); 44 | } 45 | addNewContainer(elName) { 46 | 47 | var itemToAdd = { 48 | elementName: elName, 49 | style: elName === 'Container' ? {height: 200, width: 740} : {}, 50 | containers: [], 51 | boxes: [] 52 | }; 53 | this.addNewItem(itemToAdd); 54 | } 55 | 56 | addNewCtrl(elName) { 57 | var itemToAdd = {elementName: elName}; 58 | this.addNewItem(itemToAdd); 59 | } 60 | 61 | addNewItem(itemToAdd) { 62 | var current = this.state.current.node; 63 | if (current === undefined) return; 64 | 65 | if (itemToAdd.name === undefined) itemToAdd['name'] = itemToAdd.elementName; 66 | if (itemToAdd.style === undefined) itemToAdd['style'] = {}; 67 | 68 | var type = itemToAdd.containers !== undefined ? "containers" : "boxes"; 69 | //init empty collection if needed 70 | var updated = (current[type] === undefined) ? current.set({[type]: [itemToAdd]}) : current[type].push(itemToAdd).__.parents[0]; 71 | 72 | this.currentChanged(updated); 73 | } 74 | 75 | 76 | render() { 77 | var schema = this.props.state.schema; 78 | var editorState = this.props.editorState || {}; 79 | 80 | var currentNode = this.state.current.node; 81 | var elementName = currentNode.elementName; 82 | var props = _.merge(getContainerProps(elementName),currentNode.props && currentNode.props.toJS()); 83 | 84 | 85 | return ( 86 |
87 | 88 | 89 |
90 | 93 |
94 |
95 |
96 | 101 | 106 |    107 | 108 | 114 | 122 | 123 | 124 | 130 | 142 | 143 | 144 | 145 |
146 |
147 | {/* */} 148 | 150 |
151 |
152 |
153 |
154 | 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /example/src/components/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | export default class Toolbar extends React.Component { 5 | up() { 6 | var items = this.props.current.parentNode.containers; 7 | var itemIndex = items.indexOf(this.props.current.node); 8 | if (itemIndex === 0) return; 9 | 10 | var element = items[itemIndex]; 11 | var toIndex = _.max([0, itemIndex - 1]); 12 | 13 | var updated = items.splice(itemIndex, 1).splice(toIndex, 0, element); 14 | this.props.currentChanged(updated[toIndex]); 15 | } 16 | 17 | down() { 18 | var items = this.props.current.parentNode.containers; 19 | var itemIndex = items.indexOf(this.props.current.node); 20 | if (itemIndex === items.length - 1) return; 21 | 22 | var element = items[itemIndex]; 23 | var toIndex = _.min([items.length, itemIndex + 1]); 24 | 25 | var updated = items.splice(itemIndex, 1).splice(toIndex, 0, element); 26 | this.props.currentChanged(updated[toIndex]); 27 | } 28 | 29 | removeCtrl() { 30 | var current = this.props.current.node; 31 | if (current === undefined) return; 32 | var parent = this.props.current.parentNode; 33 | if (parent === undefined) return; 34 | 35 | var items = this.isContainer() ? parent.containers : parent.boxes; 36 | 37 | //remove selected item 38 | var index = items.indexOf(current); 39 | var updated = items.splice(index, 1); 40 | 41 | //set current 42 | if (index < items.length) { 43 | this.props.currentChanged(updated[index]); 44 | } 45 | } 46 | 47 | copy() { 48 | var current = this.props.current.node; 49 | if (current === undefined) return; 50 | var parent = this.props.current.parentNode; 51 | if (parent === undefined) return; 52 | 53 | //clone 54 | var clone = _.cloneDeep(current); 55 | clone.name = 'Copy ' + clone.name; 56 | 57 | var isContainer = this.isContainer(); 58 | 59 | //move down by item height 60 | if (!isContainer) clone.style.top = (current.style.top || 0) + (current.style.height || 0); 61 | 62 | var items = isContainer ? parent.containers : parent.boxes; 63 | 64 | //add new cloned of selected item 65 | var updated = items.push(clone); 66 | 67 | //set current 68 | var index = items.indexOf(current); 69 | this.props.currentChanged(updated[index]); 70 | } 71 | isContainer(){ 72 | var current = this.props.current && this.props.current.node; 73 | return current !== undefined && current.boxes !== undefined; 74 | } 75 | 76 | 77 | render() { 78 | let {current} = this.props; 79 | 80 | var disabledCurrent = current.node === undefined || current.parentNode === undefined; 81 | var items = current.parentNode !== undefined ? current.parentNode.containers : []; 82 | var disabledMove = disabledCurrent || !this.isContainer() || items.lenght <= 1; 83 | 84 | //if first item - > disable 85 | var disabledUp = disabledMove || items.indexOf(this.props.current.node) === 0; 86 | //if last item -> disable 87 | var disabledDown = disabledMove || items.indexOf(this.props.current.node) === items.length - 1; 88 | 89 | return ( 90 | 91 |    92 | 96 | 100 |    101 | 105 | 109 | 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example/src/components/WidgetRenderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | export default (properties) => { 5 | 6 | let {node,widget,selected, current, currentChanged} = properties; 7 | if (widget === undefined) return React.DOM.span(null, 'Component ' + node.elementName + ' is not register among widgets.'); 8 | 9 | var props = node.props || {}; 10 | 11 | //propaget specific additional properties for inline editor 12 | var isInlineEdit = selected && node.elementName === 'Core.RichTextContent'; 13 | if (isInlineEdit) props = _.extend(props,{designer:true,current:current,currentChanged:currentChanged,node:node}); 14 | 15 | return React.createElement(widget,props,props.content !== undefined ? React.createElement("div",{ dangerouslySetInnerHTML: {__html: props.content } }) : null); 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /example/src/components/Widgets.js: -------------------------------------------------------------------------------- 1 | import BackgroundContainer from '../widgets/BackgroundContainer'; 2 | import Grid from '../widgets/GridWrapper'; 3 | import Cell from '../widgets/CellWrapper'; 4 | import RichTextRenderer from '../widgets/RichTextEditor'; 5 | import TextRenderer from '../widgets/TextRenderer'; 6 | 7 | 8 | export default { 9 | 'BackgroundContainer':BackgroundContainer, 10 | 'Grid':Grid, 11 | 'Cell':Cell, 12 | 13 | 'Core.TextContent':TextRenderer, 14 | 'Core.RichTextContent':RichTextRenderer 15 | }; 16 | -------------------------------------------------------------------------------- /example/src/components/containerMetadata.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const COMPONENT_METADATA = { 4 | ObjectSchema: { 5 | metaData: { 6 | settings: { 7 | fields: { 8 | defaultData: { type: 'plainJsonEditor' }, 9 | background: { type: 'bgEditor' }, 10 | pageOptions: { type: 'pageOptionsEditor' }, 11 | context: { 12 | fields: { 13 | intlData: { type: 'plainJsonEditor' }, 14 | styles: { type: 'widgetStyleEditor' }, 15 | code: { type: 'codeEditor' } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | }, 22 | Container: { 23 | metaData: { 24 | settings: { 25 | fields: { 26 | visibility: { type: 'boolean' }, 27 | startOnNewPage: { type: 'boolean' }, 28 | unbreakable: { type: 'boolean' } 29 | 30 | } 31 | } 32 | } 33 | }, 34 | BackgroundContainer: { 35 | metaData: { 36 | settings: { 37 | fields: { 38 | visibility: { type: 'boolean' }, 39 | startOnNewPage: { type: 'boolean' }, 40 | unbreakable: { type: 'boolean' }, 41 | width: { type: 'number' }, 42 | height: { type: 'number' }, 43 | background: { type: 'bgEditor' } 44 | } 45 | } 46 | } 47 | }, 48 | Repeater: { 49 | metaData: { 50 | settings: { 51 | fields: { 52 | binding: { type: 'plainJsonEditor' }, 53 | startOnNewPage: { type: 'boolean' }, 54 | unbreakable: { type: 'boolean' } 55 | } 56 | } 57 | } 58 | }, 59 | Grid: { 60 | metaData: { 61 | settings: { 62 | fields: { 63 | rowTemplate: { type: 'string' }, 64 | columnTemplate: { type: 'string' }, 65 | areasTemplate: { type: 'string' }, 66 | visibility: { type: 'boolean' }, 67 | justifyItems: { 68 | type: 'select', 69 | settings: { options: ['start', 'end', 'center', 'stretch'] } 70 | }, 71 | alignItems: { 72 | type: 'select', 73 | settings: { options: ['start', 'end', 'center', 'stretch'] } 74 | }, 75 | justifyContent: { 76 | type: 'select', 77 | settings: { options: ['start', 'end', 'center', 'stretch', 'space-around', 'space-between', 'space-evenly'] } 78 | }, 79 | alignContent: { 80 | type: 'select', 81 | settings: { options: ['start', 'end', 'center', 'stretch', 'space-around', 'space-between', 'space-evenly'] } 82 | }, 83 | background: { type: 'bgEditor' }, 84 | padding: { type: 'boxSizeEditor' }, 85 | border: { type: 'borderEditor' } 86 | } 87 | } 88 | } 89 | }, 90 | Cell: { 91 | metaData: { 92 | settings: { 93 | fields: { 94 | justifySelf: { 95 | type: 'select', 96 | settings: { options: ['start', 'end', 'center', 'stretch'] } 97 | }, 98 | alignSelf: { 99 | type: 'select', 100 | settings: { options: ['start', 'end', 'center', 'stretch'] } 101 | }, 102 | area: { type:'string'}, 103 | rowStart: {type:'number'}, 104 | rowEnd: {type: 'number'}, 105 | columnStart: {type: 'number'}, 106 | columnEnd: {type: 'number'}, 107 | columns: {type: 'string'}, 108 | rows: {type: 'string'}, 109 | background: { type: 'bgEditor' }, 110 | padding: { type: 'boxSizeEditor' }, 111 | border: { type: 'borderEditor' } 112 | } 113 | } 114 | } 115 | } 116 | }; 117 | 118 | 119 | const convertToEmptyProps = function (children) { 120 | var result = _.mapValues(children, function () { return undefined }); 121 | for (var key in children) { 122 | var fields = children[key]['fields']; 123 | if (fields === undefined) continue; 124 | result[key] = convertToEmptyProps(fields); 125 | } 126 | return result; 127 | } 128 | const CONTAINER_KEYS = _.keys(COMPONENT_METADATA); 129 | const isContainer = function (elementName) { 130 | return _.includes(CONTAINER_KEYS, elementName); 131 | } 132 | 133 | export default function (elementName) { 134 | if (!isContainer(elementName)) return; 135 | let metadata = COMPONENT_METADATA[elementName].metaData; 136 | let settings = metadata && metadata.settings || {} 137 | return convertToEmptyProps(settings.fields); 138 | } 139 | 140 | -------------------------------------------------------------------------------- /example/src/example.css: -------------------------------------------------------------------------------- 1 | ._2iUMdY{display:flex;flex-wrap:wrap;list-style:none;padding:0;margin:0;}._MzY6c{justify-content:flex-start;}._SPd4R{justify-content:center;}._1H7ntZ{justify-content:flex-end;}._13kU4I{align-items:flex-start;}._1A3OE5{align-items:center;}._LhCR5{align-items:flex-end;}._11Q4tC{padding:0px;}._7V7hx{flex:1;}._j0GFy{display:flex;}._2181U{align-self:flex-start;}._4n7J85{align-self:flex-end;}._WrnAz{align-self:center;} 2 | 3 | /*.RichEditor-root {*/ 4 | /*background: #fff;*/ 5 | /*border: 1px solid #ddd;*/ 6 | /*font-family: 'Georgia', serif;*/ 7 | /*font-size: 14px;*/ 8 | /*padding: 15px;*/ 9 | /*}*/ 10 | 11 | /*.RichEditor-editor {*/ 12 | /*border-top: 1px solid #ddd;*/ 13 | /*cursor: text;*/ 14 | /*font-size: 16px;*/ 15 | /*margin-top: 10px;*/ 16 | /*}*/ 17 | 18 | /*.RichEditor-editor .public-DraftEditorPlaceholder-root,*/ 19 | /*.RichEditor-editor .public-DraftEditor-content {*/ 20 | /*margin: 0 -15px -15px;*/ 21 | /*padding: 15px;*/ 22 | /*}*/ 23 | 24 | /*.RichEditor-editor .public-DraftEditor-content {*/ 25 | /*min-height: 100px;*/ 26 | /*}*/ 27 | 28 | /*.RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root {*/ 29 | /*display: none;*/ 30 | /*}*/ 31 | 32 | .RichEditor-editor .RichEditor-blockquote { 33 | border-left: 5px solid #eee; 34 | color: #666; 35 | font-family: 'Hoefler Text', 'Georgia', serif; 36 | font-style: italic; 37 | margin: 16px 0; 38 | padding: 10px 20px; 39 | } 40 | 41 | .RichEditor-editor .public-DraftStyleDefault-pre { 42 | background-color: rgba(0, 0, 0, 0.05); 43 | font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace; 44 | font-size: 16px; 45 | padding: 20px; 46 | } 47 | 48 | .RichEditor-controls { 49 | font-family: 'Helvetica', sans-serif; 50 | font-size: 14px; 51 | margin-bottom: 5px; 52 | user-select: none; 53 | } 54 | 55 | .RichEditor-styleButton { 56 | color: #999; 57 | cursor: pointer; 58 | margin-right: 16px; 59 | padding: 2px 0; 60 | } 61 | 62 | .RichEditor-activeButton { 63 | color: #5890ff; 64 | } 65 | 66 | .cWorkplace { 67 | border: solid 0px blue; 68 | position: absolute; 69 | width: 100%; 70 | } 71 | p { 72 | margin: 0px; 73 | } 74 | .con { 75 | min-height: 20px; 76 | min-width: 20px; 77 | border: solid 1px gray; 78 | resize: none; 79 | overflow: visible; 80 | } 81 | .con.selected { 82 | border: 1px solid red; 83 | } 84 | .con.parentSelected { 85 | border: 1px solid darkviolet; 86 | } 87 | .con.root { 88 | background-image: url("grid.png"); 89 | background-repeat: repeat; 90 | min-height: 100vh; 91 | border-width: 0px 1px 0px 0px; 92 | margin: 0px; 93 | padding: 0px; 94 | } 95 | .box { 96 | position: absolute; 97 | border: 0px dashed gray; 98 | padding: 0px; 99 | } 100 | .box.selected { 101 | border: 2px dashed red; 102 | } 103 | .resizable-handle { 104 | position: absolute; 105 | bottom: 0px; 106 | right: 0px; 107 | width: 20px; 108 | height: 20px; 109 | background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); 110 | background-position: bottom right; 111 | padding: 0 8px 8px 0; 112 | background-repeat: no-repeat; 113 | background-origin: content-box; 114 | box-sizing: border-box; 115 | cursor: se-resize; 116 | } 117 | /* the tree node's style */ 118 | .tree-view { 119 | overflow-y: hidden; 120 | } 121 | /* style for the container */ 122 | .tree-view_children { 123 | margin-left: 16px; 124 | border-left: 1px solid gold; 125 | } 126 | .tree-view_children-collapsed { 127 | display: none; 128 | } 129 | .tree-view_arrow { 130 | cursor: pointer; 131 | margin-right: 6px; 132 | display: inline-block; 133 | -webkit-user-select: none; 134 | -moz-user-select: none; 135 | -ms-user-select: none; 136 | user-select: none; 137 | font-size: 20px; 138 | } 139 | .tree-view_arrow:after { 140 | content: '▾'; 141 | } 142 | /* rotate the triangle to close it */ 143 | .tree-view_arrow-collapsed { 144 | -webkit-transform: rotate(-90deg); 145 | -moz-transform: rotate(-90deg); 146 | -ms-transform: rotate(-90deg); 147 | transform: rotate(-90deg); 148 | } 149 | .node { 150 | -moz-transition: all 0.5s; 151 | -o-transition: all 0.5s; 152 | -ms-transition: all 0.5s; 153 | -webkit-transition: all 0.5s; 154 | transition: all 0.5s; 155 | border-radius: 3px; 156 | margin: 2px; 157 | vertical-align: middle; 158 | } 159 | .node.selected { 160 | background-color: #e8efff; 161 | box-shadow: 0 0 2px #0a6aa1; 162 | } 163 | .node:hover { 164 | background-color: #dcf5f3; 165 | cursor: pointer; 166 | } 167 | .info, 168 | .node { 169 | padding: 2px 10px 2px 5px; 170 | font: 14px Helvetica, Arial, sans-serif; 171 | -webkit-user-select: none; 172 | -moz-user-select: none; 173 | -ms-user-select: none; 174 | user-select: none; 175 | } 176 | .tree-view_arrow { 177 | -moz-transition: all 0.1s; 178 | -o-transition: all 0.1s; 179 | -ms-transition: all 0.1s; 180 | -webkit-transition: all 0.1s; 181 | transition: all 0.1s; 182 | } 183 | .tree-view_arrow-empty { 184 | color: yellow; 185 | } 186 | .pageStyle { 187 | border: 1px solid gray; 188 | background-color: #ffffff; 189 | } 190 | .doublePageStyle { 191 | display: flex; 192 | display: -webkit-flex; 193 | } 194 | @media print { 195 | body { 196 | color: #000; 197 | background: #fff; 198 | margin: 0px; 199 | padding: 0px; 200 | boder: 0px; 201 | -webkit-print-color-adjust: exact; 202 | width: 100%; 203 | height: 100%; 204 | } 205 | .pageStyle { 206 | border-width: 0px; 207 | background-color: #ffffff; 208 | } 209 | .doublePageStyle { 210 | display: block; 211 | } 212 | .hidden-print { 213 | display: none !important; 214 | } 215 | } 216 | .Resizer { 217 | background: #000; 218 | opacity: .2; 219 | z-index: 1; 220 | -moz-box-sizing: border-box; 221 | -webkit-box-sizing: border-box; 222 | box-sizing: border-box; 223 | -moz-background-clip: padding; 224 | -webkit-background-clip: padding; 225 | background-clip: padding-box; 226 | } 227 | .Resizer:hover { 228 | -webkit-transition: all 2s ease; 229 | transition: all 2s ease; 230 | } 231 | .Resizer.horizontal { 232 | height: 11px; 233 | margin: -5px 0; 234 | border-top: 5px solid rgba(255, 255, 255, 0); 235 | border-bottom: 5px solid rgba(255, 255, 255, 0); 236 | cursor: row-resize; 237 | width: 100%; 238 | } 239 | .Resizer.horizontal:hover { 240 | border-top: 5px solid rgba(0, 0, 0, 0.5); 241 | border-bottom: 5px solid rgba(0, 0, 0, 0.5); 242 | } 243 | .Resizer.vertical { 244 | width: 11px; 245 | margin: 0 -5px; 246 | border-left: 5px solid rgba(255, 255, 255, 0); 247 | border-right: 5px solid rgba(255, 255, 255, 0); 248 | cursor: col-resize; 249 | height: 100%; 250 | } 251 | .Resizer.vertical:hover { 252 | border-left: 5px solid rgba(0, 0, 0, 0.5); 253 | border-right: 5px solid rgba(0, 0, 0, 0.5); 254 | } 255 | -------------------------------------------------------------------------------- /example/src/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Freezer from 'freezer-js'; 5 | import Designer from './components/Designer'; 6 | 7 | 8 | // Create a Freezer store 9 | var freezer = new Freezer({ 10 | schema: { 11 | elementName: 'ObjectSchema', 12 | name: 'New Document', 13 | containers: [] 14 | } 15 | }); 16 | 17 | // export default class App extends React.Component { 18 | // 19 | // componentDidMount() { 20 | // var me = this; 21 | // 22 | // // 2. Your app get re-rendered on any state change 23 | // freezer.on('update', function () { 24 | // me.forceUpdate() 25 | // }); 26 | // } 27 | // 28 | // render() { 29 | // // 1. Your app receives the state 30 | // var state = freezer.get(); 31 | // 32 | // return ; 33 | // } 34 | // } 35 | 36 | class AppWithHistory extends React.Component { 37 | 38 | constructor(props) { 39 | super(props); 40 | this.state = { 41 | storeHistory: [freezer.get()], 42 | currentStore: 0 43 | }; 44 | } 45 | undo() { 46 | var nextIndex = this.state.currentStore - 1; 47 | if (nextIndex < 0 ) return; 48 | freezer.set(this.state.storeHistory[nextIndex]); 49 | this.setState({currentStore: nextIndex}); 50 | } 51 | 52 | redo() { 53 | var nextIndex = this.state.currentStore + 1; 54 | if (nextIndex > this.state.storeHistory.length -1) return; 55 | freezer.set(this.state.storeHistory[nextIndex]); 56 | this.setState({currentStore: nextIndex}); 57 | } 58 | 59 | componentDidMount() { 60 | var me = this; 61 | 62 | // 2. Your app get re-rendered on any state change 63 | freezer.on('update', function ( state ) { 64 | 65 | var storeHistory, nextIndex; 66 | // Check if this state has not been set by the history 67 | if (state != me.state.storeHistory[me.state.currentStore]) { 68 | 69 | nextIndex = me.state.currentStore + 1; 70 | storeHistory = me.state.storeHistory.slice(0, nextIndex); 71 | storeHistory.push(state); 72 | 73 | // Set the state will re-render our component 74 | me.setState({ 75 | storeHistory: storeHistory, 76 | currentStore: nextIndex 77 | }); 78 | } 79 | else { 80 | // The change has been already triggered by the state, no need of re-render 81 | } 82 | 83 | //me.forceUpdate() 84 | }); 85 | } 86 | 87 | render() { 88 | // 1. Your app receives the state 89 | var state = freezer.get(); 90 | 91 | 92 | var editorState = { 93 | undo:this.undo.bind(this), 94 | redo:this.redo.bind(this), 95 | canUndo:!this.state.currentStore, 96 | canRedo:this.state.currentStore == this.state.storeHistory.length - 1 97 | }; 98 | 99 | return ; 100 | } 101 | } 102 | 103 | ReactDOM.render(, document.getElementById('app')); 104 | -------------------------------------------------------------------------------- /example/src/example.less: -------------------------------------------------------------------------------- 1 | @import (inline) "./react-flexr.css"; 2 | @import (inline) "./RichEditor.css"; 3 | 4 | .cWorkplace{ 5 | border:solid 0px blue; 6 | position:absolute; 7 | width:100%; 8 | } 9 | 10 | p { 11 | margin:0px; 12 | } 13 | .con{ 14 | min-height:20px; 15 | min-width:20px; 16 | border:solid 1px gray; 17 | resize:none; 18 | overflow:visible; 19 | } 20 | .con.selected { 21 | border: 1px solid red; 22 | } 23 | .con.parentSelected { 24 | border: 1px solid darkviolet; 25 | } 26 | .con.root { 27 | background-image: url("grid.png"); 28 | background-repeat: repeat; 29 | min-height: 100vh; 30 | border-width: 0px 1px 0px 0px; 31 | margin:0px; 32 | padding:0px; 33 | } 34 | 35 | .box { 36 | position: absolute; 37 | border: 0px dashed gray; 38 | padding: 0px; 39 | } 40 | .box.selected { 41 | border: 2px dashed red; 42 | } 43 | 44 | .resizable-handle { 45 | position: absolute; 46 | bottom:0px; 47 | right:0px; 48 | width: 20px; 49 | height: 20px; 50 | background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); 51 | background-position: bottom right; 52 | padding: 0 8px 8px 0; 53 | background-repeat: no-repeat; 54 | background-origin: content-box; 55 | box-sizing: border-box; 56 | cursor: se-resize; 57 | } 58 | 59 | 60 | /* the tree node's style */ 61 | .tree-view { 62 | overflow-y: hidden; 63 | } 64 | 65 | /* style for the container */ 66 | .tree-view_children { 67 | margin-left: 16px; 68 | border-left: 1px solid gold; 69 | } 70 | 71 | .tree-view_children-collapsed { 72 | display:none; 73 | } 74 | 75 | .tree-view_arrow { 76 | cursor: pointer; 77 | margin-right: 6px; 78 | display: inline-block; 79 | -webkit-user-select: none; 80 | -moz-user-select: none; 81 | -ms-user-select: none; 82 | user-select: none; 83 | font-size: 20px; 84 | } 85 | 86 | .tree-view_arrow:after { 87 | content: '▾'; 88 | } 89 | 90 | /* rotate the triangle to close it */ 91 | .tree-view_arrow-collapsed { 92 | -webkit-transform: rotate(-90deg); 93 | -moz-transform: rotate(-90deg); 94 | -ms-transform: rotate(-90deg); 95 | transform: rotate(-90deg); 96 | } 97 | 98 | 99 | .node { 100 | -moz-transition: all 0.5s; 101 | -o-transition: all 0.5s; 102 | -ms-transition: all 0.5s; 103 | -webkit-transition: all 0.5s; 104 | transition: all 0.5s; 105 | border-radius: 3px; 106 | margin:2px; 107 | vertical-align: middle; 108 | } 109 | .node.selected { 110 | background-color: #e8efff; 111 | box-shadow: 0 0 2px #0a6aa1; 112 | //border: solid 1px #0a6aa1 113 | } 114 | 115 | .node:hover { 116 | background-color: rgb(220, 245, 243); 117 | cursor: pointer; 118 | } 119 | 120 | .info, .node { 121 | padding: 2px 10px 2px 5px; 122 | font: 14px Helvetica, Arial, sans-serif; 123 | -webkit-user-select: none; 124 | -moz-user-select: none; 125 | -ms-user-select: none; 126 | user-select: none; 127 | } 128 | 129 | .tree-view_arrow { 130 | -moz-transition: all 0.1s; 131 | -o-transition: all 0.1s; 132 | -ms-transition: all 0.1s; 133 | -webkit-transition: all 0.1s; 134 | transition: all 0.1s; 135 | } 136 | 137 | .tree-view_arrow-empty { 138 | color: yellow; 139 | } 140 | 141 | 142 | .pageStyle { 143 | border: 1px solid gray; 144 | background-color: #ffffff; 145 | } 146 | 147 | .doublePageStyle { 148 | display: flex; 149 | display: -webkit-flex; 150 | } 151 | 152 | @media print { 153 | body { 154 | color: #000; 155 | background: #fff; 156 | margin: 0px; 157 | padding: 0px; 158 | boder:0px; 159 | //display: inline-block; 160 | -webkit-print-color-adjust: exact; 161 | width: 100%; 162 | height: 100%; 163 | } 164 | 165 | .pageStyle { 166 | border-width: 0px; 167 | background-color: #ffffff; 168 | } 169 | 170 | .doublePageStyle { 171 | display: block; 172 | } 173 | 174 | .hidden-print { 175 | display: none !important; 176 | } 177 | } 178 | 179 | 180 | .Resizer { 181 | background: #000; 182 | opacity: .2; 183 | z-index: 1; 184 | -moz-box-sizing: border-box; 185 | -webkit-box-sizing: border-box; 186 | box-sizing: border-box; 187 | -moz-background-clip: padding; 188 | -webkit-background-clip: padding; 189 | background-clip: padding-box; 190 | } 191 | 192 | .Resizer:hover { 193 | -webkit-transition: all 2s ease; 194 | transition: all 2s ease; 195 | } 196 | 197 | .Resizer.horizontal { 198 | height: 11px; 199 | margin: -5px 0; 200 | border-top: 5px solid rgba(255, 255, 255, 0); 201 | border-bottom: 5px solid rgba(255, 255, 255, 0); 202 | cursor: row-resize; 203 | width: 100%; 204 | } 205 | 206 | .Resizer.horizontal:hover { 207 | border-top: 5px solid rgba(0, 0, 0, 0.5); 208 | border-bottom: 5px solid rgba(0, 0, 0, 0.5); 209 | } 210 | 211 | .Resizer.vertical { 212 | width: 11px; 213 | margin: 0 -5px; 214 | border-left: 5px solid rgba(255, 255, 255, 0); 215 | border-right: 5px solid rgba(255, 255, 255, 0); 216 | cursor: col-resize; 217 | height: 100%; 218 | } 219 | 220 | .Resizer.vertical:hover { 221 | border-left: 5px solid rgba(0, 0, 0, 0.5); 222 | border-right: 5px solid rgba(0, 0, 0, 0.5); 223 | } 224 | 225 | -------------------------------------------------------------------------------- /example/src/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsamec/react-designer-core/1bd52e74c8551f75fe25eedf9bc99ecf3e0c4a83/example/src/grid.png -------------------------------------------------------------------------------- /example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Designer 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/src/react-flexr.css: -------------------------------------------------------------------------------- 1 | ._2iUMdY{display:flex;flex-wrap:wrap;list-style:none;padding:0;margin:0;}._MzY6c{justify-content:flex-start;}._SPd4R{justify-content:center;}._1H7ntZ{justify-content:flex-end;}._13kU4I{align-items:flex-start;}._1A3OE5{align-items:center;}._LhCR5{align-items:flex-end;}._11Q4tC{padding:0px;}._7V7hx{flex:1;}._j0GFy{display:flex;}._2181U{align-self:flex-start;}._4n7J85{align-self:flex-end;}._WrnAz{align-self:center;} 2 | -------------------------------------------------------------------------------- /example/src/widgets/BackgroundContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | import backgroundStyle from './utils/backgroundStyle'; 5 | import styleBorder from'./utils/border' 6 | 7 | export default (props) => { 8 | if (props.child) { 9 | var styles = { 10 | width: props.width, 11 | height: props.height, 12 | position: 'absolute' 13 | }; 14 | 15 | //apply custom background 16 | if (props.background !== undefined) { 17 | styles = _.extend(styles, backgroundStyle(props.background, { 18 | width: props.width, 19 | height: props.height 20 | })) 21 | } 22 | 23 | //border 24 | if (props.border !== undefined) styleBorder(styles, props.border); 25 | } 26 | 27 | return
28 |
29 |
{props.children}
30 |
31 | } 32 | -------------------------------------------------------------------------------- /example/src/widgets/BoxAlignmentStyle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | export default function (props) { 4 | 5 | const { justifyItems, justifyContent, alignItems, alignContent, alignSelf, justifySelf } = props; 6 | 7 | return { 8 | justifyItems: justifyItems, 9 | justifyContent: justifyContent, 10 | alignItems: alignItems, 11 | alignContent: alignContent, 12 | alignSelf: alignSelf, 13 | justifySelf: justifySelf 14 | } 15 | } -------------------------------------------------------------------------------- /example/src/widgets/CellWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import Cell from './GridItem'; 4 | 5 | import backgroundStyle from './utils/backgroundStyle'; 6 | import styleBorder from'./utils/border' 7 | 8 | let CellWrapper = (props) => { 9 | 10 | var styles = {}; 11 | 12 | //apply parent props 13 | var parentProps = props.parentProps || {}; 14 | 15 | if (parentProps.border !== undefined) styleBorder(styles, parentProps.border); 16 | 17 | //padding 18 | if (parentProps.padding !== undefined) { 19 | var size = parentProps.padding || {}; 20 | styles.paddingTop = size.top; 21 | styles.paddingRight = size.right; 22 | styles.paddingBottom = size.bottom; 23 | styles.paddingLeft = size.left; 24 | } 25 | 26 | var selfProps = props; 27 | 28 | //styles.width = "100%"; 29 | 30 | //apply custom background 31 | if (selfProps.background !== undefined) { 32 | styles = _.extend(styles, backgroundStyle(selfProps.background, { 33 | width: props.width, 34 | height: props.height 35 | })) 36 | } 37 | 38 | //border 39 | if (selfProps.border !== undefined) styleBorder(styles, selfProps.border); 40 | 41 | //padding 42 | if (selfProps.padding !== undefined) { 43 | var size = selfProps.padding || {}; 44 | styles.paddingTop = size.top; 45 | styles.paddingRight = size.right; 46 | styles.paddingBottom = size.bottom; 47 | styles.paddingLeft = size.left; 48 | } 49 | 50 | 51 | return 52 |
{props.children}
53 |
54 | } 55 | 56 | 57 | export default CellWrapper; 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /example/src/widgets/GridContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import BoxAligmentStyle from './BoxAlignmentStyle'; 3 | 4 | export default class GridContainer extends Component { 5 | render () { 6 | const { rowTemplate, columnTemplate, areasTemplate } = this.props 7 | 8 | let style = Object.assign({ 9 | display: 'grid', 10 | gridTemplateRows: rowTemplate, 11 | gridTemplateColumns: columnTemplate, 12 | gridTemplateAreas: areasTemplate, 13 | //...this.props.style 14 | },BoxAligmentStyle(this.props)); 15 | 16 | return
{this.props.children}
17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/src/widgets/GridItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types' 3 | import BoxAligmentStyle from './BoxAlignmentStyle'; 4 | 5 | export default class GridItem extends Component { 6 | render () { 7 | const { rowStart, rowEnd, columnStart, columnEnd } = this.props; 8 | 9 | const { 10 | columns = `${columnStart} / ${columnEnd}`, 11 | rows = `${rowStart} / ${rowEnd}`, 12 | area 13 | } = this.props 14 | 15 | let style = Object.assign({ 16 | gridColumn: columns, 17 | gridRow: rows, 18 | gridArea: area, 19 | //...this.props.style 20 | },BoxAligmentStyle(this.props)); 21 | 22 | return
{this.props.children}
23 | } 24 | } 25 | 26 | GridItem.propTypes = { 27 | area: PropTypes.string, 28 | rowStart: PropTypes.number, 29 | rowEnd: PropTypes.number, 30 | columnStart: PropTypes.number, 31 | columnEnd: PropTypes.number, 32 | columns: PropTypes.string, 33 | rows: PropTypes.string, 34 | children: PropTypes.node, 35 | style: PropTypes.object 36 | } -------------------------------------------------------------------------------- /example/src/widgets/GridWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import Grid from './GridContainer'; 4 | 5 | import backgroundStyle from './utils/backgroundStyle'; 6 | import styleBorder from'./utils/border' 7 | 8 | 9 | let GridWrapper = (props) => { 10 | 11 | var styles = {}; 12 | //styles.width = "100%"; 13 | 14 | var selfProps = props; 15 | 16 | //apply custom background 17 | if (selfProps.background !== undefined) { 18 | styles = _.extend(styles, backgroundStyle(selfProps.background, { 19 | width: props.width, 20 | height: props.height 21 | })) 22 | } 23 | 24 | //border 25 | if (selfProps.border !== undefined) styleBorder(styles, selfProps.border); 26 | 27 | //padding 28 | if (selfProps.padding !== undefined) { 29 | var size = selfProps.padding || {}; 30 | styles.paddingTop = size.top; 31 | styles.paddingRight = size.right; 32 | styles.paddingBottom = size.bottom; 33 | styles.paddingLeft = size.left; 34 | } 35 | 36 | const childrenWithProps = React.Children.map(props.children, 37 | (child) => React.cloneElement(child, {width: '100%'})); 38 | return {childrenWithProps} 39 | } 40 | 41 | export default GridWrapper; 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /example/src/widgets/RichTextEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import _ from 'lodash'; 4 | import { 5 | Editor, 6 | EditorState, 7 | ContentState, 8 | RichUtils, 9 | convertToRaw, 10 | convertFromRaw, 11 | getDefaultKeyBinding, 12 | KeyBindingUtil 13 | } from 'draft-js'; 14 | import {Overlay} from 'react-overlays'; 15 | import Toolbox from './RichTextEditorToolbox'; 16 | import RichTextRenderer from './RichTextRenderer'; 17 | 18 | const {hasCommandModifier} = KeyBindingUtil; 19 | 20 | const styleFont = function (style, fontProps) { 21 | 22 | if (style === undefined) style = {}; 23 | if (fontProps === undefined) return style; 24 | 25 | style = _.extend(style, fontProps) || {}; 26 | if (fontProps.color) style['color'] = fontProps.color.color; 27 | if (fontProps.bold) style['fontWeight'] = 'bold'; 28 | if (fontProps.italic) style['fontStyle'] = 'italic'; 29 | if (fontProps.underline) style['borderBottom'] = '1px dashed #999'; 30 | return style; 31 | } 32 | 33 | var myKeyBindingFn = function (e) { 34 | if (e.keyCode === 83 /* `S` key */ && hasCommandModifier(e)) { 35 | return 'myeditor-save'; 36 | } 37 | return getDefaultKeyBinding(e); 38 | } 39 | class RichEditor extends React.Component { 40 | constructor(props) { 41 | super(props); 42 | this.state = { 43 | editorState: EditorState.createEmpty(), 44 | showBlockStyleControls: false, 45 | showInlineStyleControls: false, 46 | show: false 47 | }; 48 | 49 | this.focus = () => this.refs.editor.focus(); 50 | //this.saveChanges = _.debounce(this.saveChanges, 100); 51 | this.onChange = (editorState) => { 52 | this.setState({editorState}); 53 | //this.props.saveChanges(editorState); 54 | //this.saveChanges(); 55 | }; 56 | 57 | 58 | this.handleKeyCommand = (command) => this._handleKeyCommand(command); 59 | this.toggleBlockType = (type) => this._toggleBlockType(type); 60 | this.toggleInlineStyle = (style) => this._toggleInlineStyle(style); 61 | } 62 | 63 | componentWillMount() { 64 | var content = this.props.node && this.props.node.props && this.props.node.props.content; 65 | this.setState({editorState: content !== undefined ? EditorState.createWithContent(convertFromRaw(content)) : EditorState.createWithContent(ContentState.createFromText('Type your content'))}); 66 | } 67 | componentWillUnmount(){ 68 | this.saveChanges() 69 | } 70 | 71 | toolboxToggle() { 72 | let show = this.state.show; 73 | //let placements = ['left', 'top', 'right', 'bottom']; 74 | let placement = this.state.placement; 75 | 76 | placement = 'top';//placements[placements.indexOf(placement)]; 77 | 78 | if (!show) { 79 | show = true; 80 | placement = 'top';//placements[0]; 81 | } 82 | else if (!placement) { 83 | show = false; 84 | } 85 | 86 | return this.setState({show, placement}); 87 | } 88 | 89 | saveChanges() { 90 | const {editorState} = this.state; 91 | var current = this.props.current; 92 | var updated = current.set('props', 93 | _.extend({ 94 | content: convertToRaw(editorState.getCurrentContent()) 95 | }, _.omit(current.props, 'content'))); 96 | 97 | //this.props.currentChanged(updated) 98 | } 99 | 100 | 101 | //componentWillUnmount(){ 102 | // var content =this.state.editorState.getCurrentContent(); 103 | // var raw = convertToRaw(content); 104 | // var updated = this.props.current.set({"props": {"content":raw}}); 105 | // this.props.currentChanged(updated); 106 | //} 107 | _handleKeyCommand(command) { 108 | const {editorState} = this.state; 109 | 110 | if (command === 'myeditor-save') { 111 | var current = this.props.current; 112 | var updated = current.set('props', 113 | _.extend({ 114 | content: convertToRaw(editorState.getCurrentContent()) 115 | }, _.omit(current.props, 'content'))); 116 | 117 | this.props.currentChanged(updated); 118 | return true; 119 | } 120 | else { 121 | const newState = RichUtils.handleKeyCommand(editorState, command); 122 | if (newState) { 123 | this.onChange(newState); 124 | return true; 125 | } 126 | } 127 | return false; 128 | } 129 | 130 | _toggleBlockType(blockType) { 131 | this.onChange( 132 | RichUtils.toggleBlockType( 133 | this.state.editorState, 134 | blockType 135 | ) 136 | ); 137 | } 138 | 139 | _toggleInlineStyle(inlineStyle) { 140 | this.onChange( 141 | RichUtils.toggleInlineStyle( 142 | this.state.editorState, 143 | inlineStyle 144 | ) 145 | ); 146 | } 147 | 148 | render() { 149 | const {editorState} = this.state; 150 | const {node} = this.props; 151 | // If the user changes block type before entering any text, we can 152 | // either style the placeholder or hide it. Let's just hide it now. 153 | let className = 'RichEditor-editor'; 154 | var contentState = editorState.getCurrentContent(); 155 | if (!contentState.hasText()) { 156 | if (contentState.getBlockMap().first().getType() !== 'unstyled') { 157 | className += ' RichEditor-hidePlaceholder'; 158 | } 159 | } 160 | var style = this.props.style || {}; 161 | 162 | styleFont(style, node.props && node.props.font); 163 | 164 | return ( 165 |
166 |
167 |
169 | 180 |
181 |
182 | this.setState({ show: false })} 185 | placement={this.state.placement} 186 | container={this} 187 | target={ () => ReactDOM.findDOMNode(this.refs.editor)} 188 | > 189 | 190 | {/* 194 | */} 195 | 199 | 200 | 201 | 202 |
203 | ); 204 | } 205 | } 206 | 207 | // Custom overrides for "code" style. 208 | const styleMap = { 209 | CODE: { 210 | backgroundColor: 'rgba(0, 0, 0, 0.05)', 211 | fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace', 212 | fontSize: 16, 213 | padding: 2 214 | } 215 | }; 216 | 217 | function getBlockStyle(block) { 218 | switch (block.getType()) { 219 | case 'blockquote': 220 | return 'RichEditor-blockquote'; 221 | default: 222 | return null; 223 | } 224 | } 225 | 226 | class StyleButton extends React.Component { 227 | constructor() { 228 | super(); 229 | this.onToggle = (e) => { 230 | e.preventDefault(); 231 | this.props.onToggle(this.props.style); 232 | }; 233 | } 234 | 235 | render() { 236 | let className = 'RichEditor-styleButton'; 237 | if (this.props.active) { 238 | className += ' RichEditor-activeButton'; 239 | } 240 | 241 | return ( 242 | 243 | {this.props.label} 244 | 245 | ); 246 | } 247 | } 248 | 249 | // const BLOCK_TYPES = [ 250 | // {label: 'H1', style: 'header-one'}, 251 | // {label: 'H2', style: 'header-two'}, 252 | // {label: 'Blockquote', style: 'blockquote'}, 253 | // {label: 'UL', style: 'unordered-list-item'}, 254 | // {label: 'OL', style: 'ordered-list-item'}, 255 | // {label: 'Code Block', style: 'code-block'} 256 | // ]; 257 | 258 | // const BlockStyleControls = (props) => { 259 | // const {editorState} = props; 260 | // const selection = editorState.getSelection(); 261 | // const blockType = editorState 262 | // .getCurrentContent() 263 | // .getBlockForKey(selection.getStartKey()) 264 | // .getType(); 265 | // 266 | // return ( 267 | //
268 | // {BLOCK_TYPES.map((type) => 269 | // 276 | // )} 277 | //
278 | // ); 279 | // }; 280 | 281 | var INLINE_STYLES = [ 282 | {label: 'Bold', style: 'BOLD'}, 283 | {label: 'Italic', style: 'ITALIC'}, 284 | {label: 'Underline', style: 'UNDERLINE'}, 285 | {label: 'Monospace', style: 'CODE'} 286 | ]; 287 | 288 | const InlineStyleControls = (props) => { 289 | var currentStyle = props.editorState.getCurrentInlineStyle(); 290 | return ( 291 |
292 | {INLINE_STYLES.map(type => 293 | 300 | )} 301 |
302 | ); 303 | }; 304 | 305 | 306 | let RichText = (props) => { 307 | var box = props.designer ? : ; 308 | return box; 309 | } 310 | // export default class RichTextComponent extends React.Component { 311 | // 312 | // componentWillReceiveProps(nextProps) { 313 | // if (nextProps.designer !== this.props.designer){ 314 | // console.log(this.props); 315 | // } 316 | // } 317 | // 318 | // saveChanges(editorState){ 319 | // //const {editorState} = this.state; 320 | // var current = this.props.current; 321 | // var updated = current.set('props', 322 | // _.extend({ 323 | // content: convertToRaw(editorState.getCurrentContent()) 324 | // }, _.omit(current.props, 'content'))); 325 | // 326 | // this.props.currentChanged(updated) 327 | // } 328 | // render (){ 329 | // var box = this.props.designer ? : ; 330 | // return box; 331 | // } 332 | // } 333 | //RichEditorRenderer.defaultProps = {content:'type your content'}; 334 | export default RichText; 335 | -------------------------------------------------------------------------------- /example/src/widgets/RichTextEditorToolbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | const TooltipStyle = { 5 | position: 'absolute', 6 | padding: '0 5px' 7 | }; 8 | const TooltipInnerStyle = { 9 | padding: '3px 8px', 10 | color: '#fff', 11 | textAlign: 'center', 12 | borderRadius: 3, 13 | backgroundColor: '#000', 14 | opacity: .75 15 | }; 16 | const TooltipArrowStyle = { 17 | position: 'absolute', 18 | width: 0, height: 0, 19 | borderRightColor: 'transparent', 20 | borderLeftColor: 'transparent', 21 | borderTopColor: 'transparent', 22 | borderBottomColor: 'transparent', 23 | borderStyle: 'solid', 24 | opacity: .75 25 | }; 26 | const PlacementStyles = { 27 | left: { 28 | tooltip: {marginLeft: -3, padding: '0 5px'}, 29 | arrow: { 30 | right: 0, marginTop: -5, borderWidth: '5px 0 5px 5px', borderLeftColor: '#000' 31 | } 32 | }, 33 | right: { 34 | tooltip: {marginRight: 3, padding: '0 5px'}, 35 | arrow: {left: 0, marginTop: -5, borderWidth: '5px 5px 5px 0', borderRightColor: '#000'} 36 | }, 37 | top: { 38 | tooltip: {marginTop: -3, padding: '5px 0'}, 39 | arrow: {bottom: 0, marginLeft: -5, borderWidth: '5px 5px 0', borderTopColor: '#000'} 40 | }, 41 | bottom: { 42 | tooltip: {marginBottom: 3, padding: '5px 0'}, 43 | arrow: {top: 0, marginLeft: -5, borderWidth: '0 5px 5px', borderBottomColor: '#000'} 44 | } 45 | }; 46 | 47 | export default class ToolTip extends React.Component { 48 | render() { 49 | let placementStyle = PlacementStyles[this.props.placement]; 50 | 51 | let { 52 | style, 53 | arrowOffsetLeft: left = placementStyle.arrow.left, 54 | arrowOffsetTop: top = placementStyle.arrow.top} = this.props; 55 | 56 | let tooltipStyle = _.extend({},TooltipStyle,placementStyle.tooltip,style); 57 | let tooltipArrowStyle = _.extend({},TooltipArrowStyle,placementStyle.arrow,{left:left,top:top}); 58 | 59 | return ( 60 |
61 |
62 |
63 | { this.props.children } 64 |
65 |
66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/src/widgets/RichTextRenderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | //import backdraft from 'backdraft-js'; 3 | import {convertToRaw,convertFromRaw} from 'draft-js'; 4 | import {stateToHTML} from 'draft-js-export-html'; 5 | import styleFont from './utils/font'; 6 | 7 | // const markup = { 8 | // 'BOLD': ['', ''], 9 | // 'ITALIC': ['', ''], 10 | // 'UNDERLINE': ['', ''], 11 | // 'CODE': ['', ''] 12 | // } 13 | 14 | let RichTextRenderer = (props) => { 15 | 16 | var style = props.style || {}; 17 | 18 | styleFont(style, props.font); 19 | 20 | //size 21 | if (props.height) style.height = props.height; 22 | if (props.width) style.width = props.width; 23 | 24 | var htmlContent = props.content !== undefined ? stateToHTML(convertFromRaw(props.content)) : ['type your content']; 25 | 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | //RichEditorRenderer.defaultProps = {content:'type your content'}; 33 | export default RichTextRenderer; 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/src/widgets/TextRenderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styleFont from './utils/font'; 4 | 5 | let TextRenderer = (props) => { 6 | 7 | var style = props.style || {}; 8 | 9 | styleFont(style, props.font); 10 | 11 | //size 12 | if (props.height) style.height = props.height; 13 | if (props.width) style.width = props.width; 14 | 15 | 16 | return ( 17 | {props.content} 18 | ); 19 | } 20 | 21 | TextRenderer.defaultProps = { 22 | content:'type your content' 23 | }; 24 | export default TextRenderer; 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/src/widgets/utils/backgroundStyle.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default function(source,panelSize) { 4 | var bg = source; 5 | var bgStyle = {}; 6 | if (bg === undefined) return bgStyle; 7 | 8 | //size 9 | if (!!bg.size) { 10 | if (panelSize !== undefined && (bg.size === "leftHalf" || bg.size === "rightHalf")) { 11 | bgStyle.backgroundSize = `${panelSize.width * 2}px ${panelSize.height}px`; 12 | bgStyle.backgroundPosition = bg.size === "leftHalf" ? '0% 0%' : '100% 0%'; 13 | //console.log(bgStyle); 14 | } 15 | else{ 16 | bgStyle.backgroundSize = bg.size; 17 | if (!!bg.position) bgStyle.backgroundPosition = bg.position; 18 | } 19 | } 20 | //gradient 21 | var bgGradient = bg.gradient; 22 | if (bgGradient !== undefined) { 23 | //gradient 24 | if (bgGradient.stops !== undefined) { 25 | var gradientStops = _.reduce(bgGradient.stops, function (memo, stop) { 26 | return memo + ', ' + stop.color + ' ' + (stop.offset * 100) + '%' 27 | }, ''); 28 | 29 | var orientation = bgGradient.orientation || 'top'; 30 | var grandientType = bgGradient.orientation === 'center, ellipse cover' ? '-webkit-radial-gradient' : '-webkit-linear-gradient'; 31 | bgStyle.background = grandientType + '(' + orientation + gradientStops + ')'; 32 | } 33 | } 34 | 35 | //color 36 | var bgColor = bg.color; 37 | if (bgColor !== undefined) { 38 | if (!!bgColor.color) bgStyle.backgroundColor = bgColor.color; 39 | if (!!bgColor.alpha) bgStyle.opacity = bgColor.alpha / 100; 40 | } 41 | 42 | if (!!bg.image) bgStyle.backgroundImage = 'url(' + bg.image + ')'; 43 | if (!!bg.repeat) bgStyle.backgroundRepeat = bg.repeat; 44 | if (!!bg.attachment) bgStyle.backgroundAttachment = bg.attachment; 45 | 46 | var filter = bg.filter || {}; 47 | var cssFilter = ""; 48 | if (!!filter.blur) cssFilter += ' blur(' + filter.blur + 'px)'; 49 | if (!!filter.brightness) cssFilter += ' brightness(' + filter.brightness + '%)'; 50 | if (!!filter.contrast) cssFilter += ' contrast(' + filter.contrast + '%)'; 51 | if (!!filter.grayscale) cssFilter += ' grayscale(' + filter.grayscale + '%)'; 52 | if (!!filter.hueRotate) cssFilter += ' hue-rotate(' + filter.hueRotate + 'deg)'; 53 | if (!!filter.invert) cssFilter += ' invert(' + filter.invert + '%)'; 54 | if (!!filter.opacity) cssFilter += ' opacity(' + filter.opacity + '%)'; 55 | if (!!filter.saturate) cssFilter += ' saturate(' + filter.saturate + '%)'; 56 | if (!!filter.sepia) cssFilter += ' sepia(' + filter.sepia + '%)'; 57 | 58 | if (!!cssFilter) { 59 | bgStyle.WebkitFilter = cssFilter; 60 | bgStyle.filter = cssFilter; 61 | } 62 | //bgStyle.position = 'absolute'; 63 | return bgStyle; 64 | } 65 | -------------------------------------------------------------------------------- /example/src/widgets/utils/border.js: -------------------------------------------------------------------------------- 1 | export default function styleBorder(style, border) { 2 | 3 | if (style === undefined) style = {}; 4 | if (border === undefined) return style; 5 | 6 | if (border.width) style.borderWidth = border.width; 7 | if (border.radius) style.borderRadius = border.radius; 8 | if (border.color) style.borderColor = border.color && border.color.color; 9 | style.borderStyle = border.style || 'solid'; 10 | return style; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /example/src/widgets/utils/font.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default function styleFont(style, fontProps) { 4 | 5 | if (style === undefined) style = {}; 6 | if (fontProps === undefined) return style; 7 | 8 | style = _.extend(style,fontProps) || {}; 9 | if (fontProps.color) style['color'] = fontProps.color.color; 10 | if (fontProps.bold) style['fontWeight'] = 'bold'; 11 | if (fontProps.italic) style['fontStyle'] = 'italic'; 12 | if (fontProps.underline) style['borderBottom'] = '1px dashed #999'; 13 | return style; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var initGulpTasks = require('react-component-gulp-tasks'); 3 | 4 | /** 5 | * Tasks are added by the react-component-gulp-tasks package 6 | * 7 | * See https://github.com/JedWatson/react-component-gulp-tasks 8 | * for documentation. 9 | * 10 | * You can also add your own additional gulp tasks if you like. 11 | */ 12 | 13 | var taskConfig = { 14 | 15 | component: { 16 | name: 'Designer', 17 | dependencies: [ 18 | 'classnames', 19 | 'react', 20 | 'react-dom' 21 | ], 22 | lib: 'lib' 23 | }, 24 | 25 | example: { 26 | src: 'example/src', 27 | dist: 'example/dist', 28 | port: 8002, 29 | files: [ 30 | 'index.html', 31 | 'grid.png', 32 | '.gitignore' 33 | ], 34 | scripts: [ 35 | 'example.js' 36 | ], 37 | less: [ 38 | 'example.less' 39 | ] 40 | } 41 | 42 | }; 43 | 44 | initGulpTasks(gulp, taskConfig); 45 | -------------------------------------------------------------------------------- /lib/Designer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 8 | 9 | var _componentsWorkplaceJs = require('./components/Workplace.js'); 10 | 11 | var _componentsWorkplaceJs2 = _interopRequireDefault(_componentsWorkplaceJs); 12 | 13 | var _componentsObjectBrowserJs = require('./components/ObjectBrowser.js'); 14 | 15 | var _componentsObjectBrowserJs2 = _interopRequireDefault(_componentsObjectBrowserJs); 16 | 17 | exports['default'] = { 18 | Workplace: _componentsWorkplaceJs2['default'], 19 | ObjectBrowser: _componentsObjectBrowserJs2['default'] 20 | }; 21 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/components/ObjectBrowser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 16 | 17 | var _react = require('react'); 18 | 19 | var _react2 = _interopRequireDefault(_react); 20 | 21 | var _reactTreeview = require('react-treeview'); 22 | 23 | var _reactTreeview2 = _interopRequireDefault(_reactTreeview); 24 | 25 | var _lodash = require('lodash'); 26 | 27 | var _lodash2 = _interopRequireDefault(_lodash); 28 | 29 | var _classnames = require('classnames'); 30 | 31 | var _classnames2 = _interopRequireDefault(_classnames); 32 | 33 | var CONTAINER_KEYS = ["ObjectSchema", "Container", "Repeater", "Grid", "Cell", "BackgroundContainer"]; 34 | 35 | var ObjectBrowser = (function (_React$Component) { 36 | _inherits(ObjectBrowser, _React$Component); 37 | 38 | function ObjectBrowser(props) { 39 | _classCallCheck(this, ObjectBrowser); 40 | 41 | _get(Object.getPrototypeOf(ObjectBrowser.prototype), 'constructor', this).call(this, props); 42 | this.state = { filterText: '' }; 43 | } 44 | 45 | _createClass(ObjectBrowser, [{ 46 | key: 'handleUserInput', 47 | value: function handleUserInput(e) { 48 | this.setState({ 49 | filterText: e.target.value 50 | }); 51 | } 52 | }, { 53 | key: 'executeAction', 54 | value: function executeAction(action, args) { 55 | if (action === "onDragStart") { 56 | this.currentItem = args; 57 | return; 58 | } 59 | if (action === "onDrop") { 60 | this.move(this.currentItem, args); 61 | this.currentItem = undefined; 62 | return; 63 | } 64 | } 65 | }, { 66 | key: 'move', 67 | value: function move(from, to) { 68 | //console.log(from.node.name + " -> " + to.node.name); 69 | // transact returns a mutable object 70 | // to make all the local changes 71 | 72 | //find source 73 | var source = from.node; 74 | var isContainer = _lodash2['default'].includes(CONTAINER_KEYS, source.elementName); 75 | 76 | //move source to target - do it in transaction 77 | var targetArray = isContainer ? to.node.containers : to.node.boxes; 78 | targetArray.transact().push(from.node); 79 | 80 | //remove source - do it in transaction 81 | var sourceParent = isContainer ? from.parentNode.containers : from.parentNode.boxes; 82 | var indexToRemove = sourceParent.indexOf(source); 83 | //console.log(indexToRemove); 84 | if (indexToRemove !== -1) { 85 | sourceParent.transact().splice(indexToRemove, 1); 86 | } 87 | 88 | // all the changes are made at once 89 | targetArray.run(); 90 | sourceParent.run(); 91 | 92 | // use it as a normal array 93 | //trans[0] = 1000; // [1000, 1, 2, ..., 999] 94 | } 95 | }, { 96 | key: 'render', 97 | value: function render() { 98 | var _this = this; 99 | 100 | var classes = (0, _classnames2['default'])({ 101 | 'node': true, 102 | 'selected': this.props.current.node === this.props.rootNode 103 | }); 104 | var path = 'schema'; 105 | return _react2['default'].createElement( 106 | 'div', 107 | null, 108 | _react2['default'].createElement( 109 | 'div', 110 | { className: 'form-group' }, 111 | _react2['default'].createElement('input', { type: 'search', className: 'form-control', placeholder: 'Search for...', onChange: this.handleUserInput.bind(this) }) 112 | ), 113 | _react2['default'].createElement( 114 | 'div', 115 | { className: classes, onClick: function (e) { 116 | return _this.props.currentChanged(_this.props.rootNode, path); 117 | } }, 118 | this.props.rootNode.name 119 | ), 120 | this.props.rootNode.containers.length === 0 ? _react2['default'].createElement( 121 | 'span', 122 | null, 123 | 'No objects to show.' 124 | ) : _react2['default'].createElement(TreeNode, { key: 'root', path: path, node: this.props.rootNode, current: this.props.current, 125 | currentChanged: this.props.currentChanged.bind(this), filterText: this.state.filterText, 126 | executeAction: this.executeAction.bind(this) }) 127 | ); 128 | } 129 | }]); 130 | 131 | return ObjectBrowser; 132 | })(_react2['default'].Component); 133 | 134 | exports['default'] = ObjectBrowser; 135 | ; 136 | 137 | var TreeNode = (function (_React$Component2) { 138 | _inherits(TreeNode, _React$Component2); 139 | 140 | function TreeNode() { 141 | _classCallCheck(this, TreeNode); 142 | 143 | _get(Object.getPrototypeOf(TreeNode.prototype), 'constructor', this).apply(this, arguments); 144 | } 145 | 146 | _createClass(TreeNode, [{ 147 | key: 'handleClick', 148 | value: function handleClick(node, path) { 149 | this.props.currentChanged(node, path); 150 | } 151 | 152 | //TODO: optimize -> now each node starts its own tree traversal 153 | }, { 154 | key: 'hideNode', 155 | value: function hideNode(node, filterText) { 156 | var trav = function trav(node) { 157 | var containers = node.containers; 158 | var boxes = node.boxes; 159 | var anyBoxes = _lodash2['default'].some(boxes, function (item) { 160 | return item.name.toLowerCase().indexOf(filterText.toLowerCase()) !== -1; 161 | }); 162 | if (anyBoxes) return true; 163 | if (node.name.indexOf(filterText) !== -1) return true; 164 | //recursion condtion stop 165 | var childrenBoxes = false; 166 | for (var i in containers) { 167 | //recursion step 168 | childrenBoxes = trav(containers[i]); 169 | if (childrenBoxes) return true; 170 | } 171 | return false; 172 | }; 173 | 174 | return !trav(node); 175 | } 176 | }, { 177 | key: 'shouldComponentUpdate', 178 | value: function shouldComponentUpdate(nextProps) { 179 | return true; 180 | // The comparison is fast, and we won't render the component if 181 | // it does not need it. This is a huge gain in performance. 182 | //var current = this.props.current.node; 183 | //var nextCurrent = nextProps.current.node; 184 | //return this.props.filterText != nextProps.filterText || this.props.nodes != nextProps.nodes || (current!==undefined && nextCurrent !==undefined && current.name != nextCurrent.name); 185 | } 186 | }, { 187 | key: 'render', 188 | value: function render() { 189 | var containers = this.props.node.containers || []; 190 | 191 | return _react2['default'].createElement( 192 | 'div', 193 | null, 194 | containers.map(function (node, i) { 195 | if (this.hideNode(node, this.props.filterText)) return; 196 | 197 | var onDragStart = (function (e) { 198 | console.log('drag started'); 199 | e.stopPropagation(); 200 | var draggingItem = { 201 | node: node, 202 | parentNode: this.props.node }; 203 | this.props.executeAction("onDragStart", draggingItem); 204 | }).bind(this); 205 | var onDragEnter = (function (e) { 206 | e.preventDefault(); // Necessary. Allows us to drop. 207 | e.stopPropagation(); 208 | //window.event.returnValue=false; 209 | //if (this.props.dragging.type !== this.props.item.type || this.props.dragging.id !== this.props.item.id) { 210 | var dropCandidate = { node: node, parentNode: this.props.node }; 211 | // var self = this; 212 | this.props.executeAction("dropPossible", dropCandidate); 213 | //} 214 | }).bind(this); 215 | var onDragOver = (function (e) { 216 | e.preventDefault(); // Necessary. Allows us to drop. 217 | e.stopPropagation(); 218 | //window.event.returnValue=false; 219 | }).bind(this); 220 | var onDrop = (function (e) { 221 | e.preventDefault(); 222 | e.stopPropagation(); 223 | var dropPlace = { node: node, parentNode: this.props.node }; 224 | this.props.executeAction("onDrop", dropPlace); 225 | }).bind(this); 226 | 227 | var type = node.elementName; 228 | 229 | var containers = node.containers || []; 230 | var boxes = node.boxes || []; 231 | 232 | var selected = this.props.current.node === node; 233 | var parentSelected = this.props.current.parentNode === node; 234 | var path = this.props.path + '.containers[' + i + ']'; 235 | 236 | var classes = (0, _classnames2['default'])({ 237 | 'node': true, 238 | 'selected': selected, 239 | 'parentSelected': this.props.parentSelected 240 | }); 241 | 242 | var label = _react2['default'].createElement( 243 | 'span', 244 | { draggable: 'true', onDragEnter: onDragEnter, 245 | onDragStart: onDragStart, 246 | onDragOver: onDragOver, onDrop: onDrop, className: classes, onClick: this.handleClick.bind(this, node, path) }, 247 | node.name 248 | ); 249 | return _react2['default'].createElement( 250 | _reactTreeview2['default'], 251 | { key: type + '|' + i, nodeLabel: label, defaultCollapsed: false }, 252 | _react2['default'].createElement(TreeNode, { key: node.name + '|' + i, node: node, path: path, current: this.props.current, currentChanged: this.props.currentChanged.bind(this), filterText: this.props.filterText, executeAction: this.props.executeAction.bind(this) }), 253 | boxes.map(function (box, j) { 254 | 255 | var onDragStart1 = (function (e) { 256 | console.log('drag started'); 257 | e.stopPropagation(); 258 | var draggingItem = { 259 | node: box, 260 | parentNode: node }; 261 | this.props.executeAction("onDragStart", draggingItem); 262 | }).bind(this); 263 | 264 | if (box.name.toLowerCase().indexOf(this.props.filterText.toLowerCase()) === -1) { 265 | return; 266 | } 267 | 268 | var boxPath = path + '.boxes[' + j + ']'; 269 | var classes = (0, _classnames2['default'])({ 270 | 'node': true, 271 | 'selected': this.props.current.node === box 272 | }); 273 | return _react2['default'].createElement( 274 | 'div', 275 | { draggable: 'true', className: classes, onDragStart: onDragStart1, onClick: this.handleClick.bind(this, box, boxPath), key: box.name + j }, 276 | _react2['default'].createElement( 277 | 'span', 278 | null, 279 | box.name 280 | ) 281 | ); 282 | }, this) 283 | ); 284 | }, this) 285 | ); 286 | } 287 | }]); 288 | 289 | return TreeNode; 290 | })(_react2['default'].Component); 291 | 292 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/components/Workplace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 16 | 17 | var _propTypes = require('prop-types'); 18 | 19 | var _propTypes2 = _interopRequireDefault(_propTypes); 20 | 21 | var _lodash = require('lodash'); 22 | 23 | var _lodash2 = _interopRequireDefault(_lodash); 24 | 25 | var _reactDndHtml5Backend = require('react-dnd-html5-backend'); 26 | 27 | var _reactDndHtml5Backend2 = _interopRequireDefault(_reactDndHtml5Backend); 28 | 29 | var _reactDnd = require('react-dnd'); 30 | 31 | var _transhand = require('transhand'); 32 | 33 | var _workplaceContainer = require('./../workplace/Container'); 34 | 35 | var _workplaceContainer2 = _interopRequireDefault(_workplaceContainer); 36 | 37 | var _utilBackgroundStyle = require('../util/backgroundStyle'); 38 | 39 | var _utilBackgroundStyle2 = _interopRequireDefault(_utilBackgroundStyle); 40 | 41 | var React = require('react'); 42 | 43 | var DEFAULT_TRANSFORM = { 44 | tx: 0, ty: 0, //translate in px 45 | sx: 1, sy: 1, //scale 46 | rz: 0, //rotation in radian 47 | ox: 0.5, oy: 0.5 //transform origin 48 | }; 49 | 50 | var DEFAULT_ROOT_PATH = 'path'; 51 | 52 | var Workplace = (function (_React$Component) { 53 | _inherits(Workplace, _React$Component); 54 | 55 | function Workplace(props) { 56 | _classCallCheck(this, Workplace); 57 | 58 | _get(Object.getPrototypeOf(Workplace.prototype), 'constructor', this).call(this, props); 59 | this.state = {}; 60 | } 61 | 62 | _createClass(Workplace, [{ 63 | key: 'getChildContext', 64 | value: function getChildContext() { 65 | return { snapGrid: this.props.snapGrid }; 66 | } 67 | }, { 68 | key: 'handleChange', 69 | value: function handleChange(change) { 70 | var currentNode = this.props.current.node; 71 | if (currentNode == undefined) return; 72 | 73 | var style = currentNode.style; 74 | if (style === undefined) return; 75 | 76 | //resolution strategy -> defaultTransform -> style.transform -> change 77 | var transform = _lodash2['default'].merge(_lodash2['default'].merge(_lodash2['default'].clone(DEFAULT_TRANSFORM), style.transform), change); 78 | 79 | var updated = currentNode.set('style', _lodash2['default'].extend(_lodash2['default'].clone(style), { 'transform': transform })); 80 | this.props.currentChanged(updated); 81 | } 82 | }, { 83 | key: 'currentChanged', 84 | value: function currentChanged(node, path, domEl) { 85 | if (this.props.currentChanged !== undefined) this.props.currentChanged(node, path); 86 | this.setState({ 87 | currentDOMNode: domEl 88 | }); 89 | } 90 | }, { 91 | key: 'render', 92 | value: function render() { 93 | var _props = this.props; 94 | var schema = _props.schema; 95 | var current = _props.current; 96 | var currentChanged = _props.currentChanged; 97 | var dataContext = _props.dataContext; 98 | 99 | var handleClick = function handleClick() { 100 | if (currentChanged !== undefined) currentChanged(schema, DEFAULT_ROOT_PATH); 101 | }; 102 | 103 | var style = current.node && current.node.style || {}; 104 | var transform = _lodash2['default'].merge(_lodash2['default'].clone(DEFAULT_TRANSFORM), style.transform); 105 | 106 | var ctx = schema.props && schema.props.context || {}; 107 | var customStyles = ctx['styles'] || {}; 108 | var code = ctx['code'] && ctx['code'].compiled; 109 | var customCode = !!code ? eval(code) : undefined; 110 | 111 | //append shared code to data context 112 | if (dataContext !== undefined) dataContext.customCode = customCode; 113 | 114 | var context = { 115 | styles: customStyles, 116 | customCode: customCode 117 | }; 118 | 119 | var bg = schema.props && schema.props.background || {}; 120 | var bgStyle = (0, _utilBackgroundStyle2['default'])(bg); 121 | 122 | bgStyle.position = 'absolute'; 123 | bgStyle.width = '100%'; 124 | bgStyle.height = '100%'; 125 | bgStyle.zIndex = -1; 126 | 127 | var component = React.createElement(_workplaceContainer2['default'], { 128 | containers: schema.containers, 129 | boxes: schema.boxes, 130 | currentChanged: this.currentChanged.bind(this), 131 | current: current, 132 | path: DEFAULT_ROOT_PATH, 133 | handleClick: handleClick, 134 | isRoot: true, 135 | node: schema, 136 | dataBinder: dataContext, 137 | ctx: context, 138 | widgets: this.props.widgets, 139 | widgetRenderer: this.props.widgetRenderer 140 | }); 141 | 142 | return React.createElement( 143 | 'div', 144 | { className: 'cWorkplace' }, 145 | React.createElement('div', { style: bgStyle }), 146 | component, 147 | this.state.currentDOMNode !== undefined ? React.createElement(_transhand.CSSTranshand, { transform: transform, deTarget: this.state.currentDOMNode, 148 | onChange: this.handleChange.bind(this) }) : null 149 | ); 150 | } 151 | }]); 152 | 153 | return Workplace; 154 | })(React.Component); 155 | 156 | ; 157 | Workplace.childContextTypes = { snapGrid: _propTypes2['default'].arrayOf(_propTypes2['default'].number) }; 158 | exports['default'] = (0, _reactDnd.DragDropContext)(_reactDndHtml5Backend2['default'])(Workplace); 159 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/util/ItemTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | exports['default'] = { 7 | BLOCK: 'block', 8 | IMAGE: 'image', 9 | BOX: 'box', 10 | CONTAINER: 'container', 11 | RESIZABLE_HANDLE: 'resize', 12 | TREE_NODE: 'treeNode' 13 | }; 14 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/util/backgroundStyle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 8 | 9 | var _lodash = require('lodash'); 10 | 11 | var _lodash2 = _interopRequireDefault(_lodash); 12 | 13 | exports["default"] = function (source, panelSize) { 14 | var bg = source; 15 | var bgStyle = {}; 16 | if (bg === undefined) return bgStyle; 17 | 18 | //size 19 | if (!!bg.size) { 20 | if (panelSize !== undefined && (bg.size === "leftHalf" || bg.size === "rightHalf")) { 21 | bgStyle.backgroundSize = panelSize.width * 2 + "px " + panelSize.height + "px"; 22 | bgStyle.backgroundPosition = bg.size === "leftHalf" ? '0% 0%' : '100% 0%'; 23 | //console.log(bgStyle); 24 | } else { 25 | bgStyle.backgroundSize = bg.size; 26 | if (!!bg.position) bgStyle.backgroundPosition = bg.position; 27 | } 28 | } 29 | //gradient 30 | var bgGradient = bg.gradient; 31 | if (bgGradient !== undefined) { 32 | //gradient 33 | if (bgGradient.stops !== undefined) { 34 | var gradientStops = _lodash2["default"].reduce(bgGradient.stops, function (memo, stop) { 35 | return memo + ', ' + stop.color + ' ' + stop.offset * 100 + '%'; 36 | }, ''); 37 | 38 | var orientation = bgGradient.orientation || 'top'; 39 | var grandientType = bgGradient.orientation === 'center, ellipse cover' ? '-webkit-radial-gradient' : '-webkit-linear-gradient'; 40 | bgStyle.background = grandientType + '(' + orientation + gradientStops + ')'; 41 | } 42 | } 43 | 44 | //color 45 | var bgColor = bg.color; 46 | if (bgColor !== undefined) { 47 | if (!!bgColor.color) bgStyle.backgroundColor = bgColor.color; 48 | if (!!bgColor.alpha) bgStyle.opacity = bgColor.alpha / 100; 49 | } 50 | 51 | if (!!bg.image) bgStyle.backgroundImage = 'url(' + bg.image + ')'; 52 | if (!!bg.repeat) bgStyle.backgroundRepeat = bg.repeat; 53 | if (!!bg.attachment) bgStyle.backgroundAttachment = bg.attachment; 54 | 55 | var filter = bg.filter || {}; 56 | var cssFilter = ""; 57 | if (!!filter.blur) cssFilter += ' blur(' + filter.blur + 'px)'; 58 | if (!!filter.brightness) cssFilter += ' brightness(' + filter.brightness + '%)'; 59 | if (!!filter.contrast) cssFilter += ' contrast(' + filter.contrast + '%)'; 60 | if (!!filter.grayscale) cssFilter += ' grayscale(' + filter.grayscale + '%)'; 61 | if (!!filter.hueRotate) cssFilter += ' hue-rotate(' + filter.hueRotate + 'deg)'; 62 | if (!!filter.invert) cssFilter += ' invert(' + filter.invert + '%)'; 63 | if (!!filter.opacity) cssFilter += ' opacity(' + filter.opacity + '%)'; 64 | if (!!filter.saturate) cssFilter += ' saturate(' + filter.saturate + '%)'; 65 | if (!!filter.sepia) cssFilter += ' sepia(' + filter.sepia + '%)'; 66 | 67 | if (!!cssFilter) { 68 | bgStyle.WebkitFilter = cssFilter; 69 | bgStyle.filter = cssFilter; 70 | } 71 | //bgStyle.position = 'absolute'; 72 | return bgStyle; 73 | }; 74 | 75 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /lib/util/generateCssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | exports['default'] = generateCssTransform; 7 | 8 | function generateCssTransform(transform) { 9 | var cssTransform = ''; 10 | 11 | if (transform.tx !== undefined) cssTransform += ' translateX(' + transform.tx + 'px)'; 12 | if (transform.ty !== undefined) cssTransform += ' translateY(' + transform.ty + 'px)'; 13 | if (transform.rz !== undefined) cssTransform += ' rotate(' + transform.rz + 'rad)'; 14 | if (transform.sx !== undefined) cssTransform += ' scaleX(' + transform.sx + ')'; 15 | if (transform.sy !== undefined) cssTransform += ' scaleY(' + transform.sy + ')'; 16 | 17 | return cssTransform; 18 | } 19 | 20 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/workplace/Box.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 16 | 17 | var _react = require('react'); 18 | 19 | var _react2 = _interopRequireDefault(_react); 20 | 21 | var _propTypes = require('prop-types'); 22 | 23 | var _propTypes2 = _interopRequireDefault(_propTypes); 24 | 25 | var _reactDom = require('react-dom'); 26 | 27 | var _reactDom2 = _interopRequireDefault(_reactDom); 28 | 29 | var _reactDnd = require('react-dnd'); 30 | 31 | var _lodash = require('lodash'); 32 | 33 | var _lodash2 = _interopRequireDefault(_lodash); 34 | 35 | var _classnames = require('classnames'); 36 | 37 | var _classnames2 = _interopRequireDefault(_classnames); 38 | 39 | var _ResizeContainerJs = require('./ResizeContainer.js'); 40 | 41 | var _ResizeContainerJs2 = _interopRequireDefault(_ResizeContainerJs); 42 | 43 | var _utilItemTypesJs = require('../util/ItemTypes.js'); 44 | 45 | var _utilItemTypesJs2 = _interopRequireDefault(_utilItemTypesJs); 46 | 47 | var _utilGenerateCssTransform = require('../util/generateCssTransform'); 48 | 49 | var _utilGenerateCssTransform2 = _interopRequireDefault(_utilGenerateCssTransform); 50 | 51 | /** 52 | * Implements the drag source contract. 53 | */ 54 | var source = { 55 | beginDrag: function beginDrag(props, monitor, component) { 56 | return { 57 | //effectAllowed: DropEffects.MOVE, 58 | item: component.props 59 | }; 60 | } 61 | }; 62 | 63 | /** 64 | * Specifies the props to inject into your component. 65 | */ 66 | function collect(connect, monitor) { 67 | return { 68 | connectDragSource: connect.dragSource(), 69 | isDragging: monitor.isDragging() 70 | }; 71 | } 72 | 73 | var propTypes = { 74 | // Injected by React DnD: 75 | isDragging: _propTypes2['default'].bool.isRequired, 76 | connectDragSource: _propTypes2['default'].func.isRequired 77 | }; 78 | 79 | var Box = (function (_React$Component) { 80 | _inherits(Box, _React$Component); 81 | 82 | function Box() { 83 | _classCallCheck(this, Box); 84 | 85 | _get(Object.getPrototypeOf(Box.prototype), 'constructor', this).apply(this, arguments); 86 | } 87 | 88 | _createClass(Box, [{ 89 | key: 'handleDoubleClick', 90 | value: function handleDoubleClick(e) { 91 | e.stopPropagation(); 92 | if (this.props.currentChanged !== undefined) this.props.currentChanged(this.props.node, this.props.path, _reactDom2['default'].findDOMNode(this)); 93 | } 94 | }, { 95 | key: 'handleClick', 96 | value: function handleClick(e) { 97 | e.stopPropagation(); 98 | if (this.props.currentChanged !== undefined) this.props.currentChanged(this.props.node, this.props.path); 99 | } 100 | }, { 101 | key: 'shouldComponentUpdate', 102 | value: function shouldComponentUpdate(nextProps) { 103 | 104 | // The comparison is fast, and we won't render the component if 105 | // it does not need it. This is a huge gain in performance. 106 | var box = this.props.node; 107 | var update = box !== nextProps.node || this.props.selected != nextProps.selected; 108 | //console.log(nextProps.node.name + ' : ' + update); 109 | if (update) return update; 110 | 111 | //test -> widget custom style changed 112 | var propsStyles = this.props.ctx.styles; 113 | var nextPropsStyles = nextProps.ctx.styles; 114 | update = (propsStyles && propsStyles[box.elementName]) !== (nextPropsStyles && nextPropsStyles[box.elementName]); 115 | 116 | return update; 117 | } 118 | 119 | // componentWillReceiveProps(nextProps){ 120 | // var index = this.props.index; 121 | // for (var action of ['right','left','up','down']) 122 | // { 123 | // if (nextProps.selected)this.props.bindShortcut(action, this.props.moveBox.bind(this,index,action));//: this.props.unbindShortcut(action); 124 | // } 125 | // } 126 | }, { 127 | key: 'render', 128 | value: function render() { 129 | var _props = this.props; 130 | var widgets = _props.widgets; 131 | var widgetRenderer = _props.widgetRenderer; 132 | var selected = _props.selected; 133 | var node = _props.node; 134 | var dataBinder = _props.dataBinder; 135 | var currentChanged = _props.currentChanged; 136 | var index = _props.index; 137 | var _props2 = this.props; 138 | var isDragging = _props2.isDragging; 139 | var connectDragSource = _props2.connectDragSource; 140 | var item = _props2.item; 141 | 142 | //prepare styles 143 | var classes = (0, _classnames2['default'])({ 144 | 'box': true, 145 | 'selected': selected 146 | }); 147 | 148 | //clone node 149 | var box = node.toJS(); 150 | 151 | //document custom style 152 | var ctx = this.props.ctx || {}; 153 | var customStyle = ctx["styles"] && ctx["styles"][box.elementName]; 154 | 155 | //specific props resolution rule -> propagate width and height from style to widget props 156 | var boxProps = box.props || {}; 157 | var boxStyle = box.style || {}; 158 | if (!boxProps.width && !!boxStyle.width) boxProps.width = boxStyle.width; 159 | if (!boxProps.height && !!boxStyle.height) boxProps.height = boxStyle.height; 160 | 161 | var boxComponent = widgetRenderer !== undefined && widgets !== undefined ? _react2['default'].createElement(widgetRenderer, { 162 | tabIndex: index, 163 | widget: widgets[box.elementName], 164 | node: box, 165 | dataBinder: dataBinder, 166 | customStyle: customStyle, 167 | customCode: ctx['customCode'], 168 | designer: true, 169 | current: node, 170 | currentChanged: currentChanged, 171 | selected: selected 172 | }, null) : _react2['default'].createElement( 173 | 'div', 174 | null, 175 | 'No widget renderer or widget factory provided.' 176 | ); 177 | 178 | //create style 179 | var styles = _lodash2['default'].extend({ position: this.props.position }, boxStyle); 180 | if (boxStyle.transform !== undefined) styles['transform'] = (0, _utilGenerateCssTransform2['default'])(boxStyle.transform); 181 | 182 | //wrap with div double click for transhand transformation 183 | if (box.elementName !== "Core.RichTextContent") boxComponent = _react2['default'].createElement( 184 | 'div', 185 | { onDoubleClick: this.handleDoubleClick.bind(this) }, 186 | boxComponent 187 | ); 188 | 189 | return connectDragSource(_react2['default'].createElement( 190 | 'div', 191 | { style: styles, className: classes, onClick: this.handleClick.bind(this) }, 192 | _react2['default'].createElement( 193 | _ResizeContainerJs2['default'], 194 | { node: node, currentChanged: currentChanged }, 195 | boxComponent 196 | ) 197 | )); 198 | } 199 | }]); 200 | 201 | return Box; 202 | })(_react2['default'].Component); 203 | 204 | ; 205 | 206 | Box.propTypes = propTypes; 207 | // Export the wrapped component: 208 | exports['default'] = (0, _reactDnd.DragSource)(_utilItemTypesJs2['default'].BOX, source, collect)(Box); 209 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/workplace/Container.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 16 | 17 | var _react = require('react'); 18 | 19 | var _react2 = _interopRequireDefault(_react); 20 | 21 | var _propTypes = require('prop-types'); 22 | 23 | var _propTypes2 = _interopRequireDefault(_propTypes); 24 | 25 | var _utilItemTypesJs = require('../util/ItemTypes.js'); 26 | 27 | var _utilItemTypesJs2 = _interopRequireDefault(_utilItemTypesJs); 28 | 29 | var _reactDnd = require('react-dnd'); 30 | 31 | var _lodash = require('lodash'); 32 | 33 | var _lodash2 = _interopRequireDefault(_lodash); 34 | 35 | var _classnames = require('classnames'); 36 | 37 | var _classnames2 = _interopRequireDefault(_classnames); 38 | 39 | var _Box = require('./Box'); 40 | 41 | var _Box2 = _interopRequireDefault(_Box); 42 | 43 | var _ResizableHandleJs = require('./ResizableHandle.js'); 44 | 45 | var _ResizableHandleJs2 = _interopRequireDefault(_ResizableHandleJs); 46 | 47 | var HANDLE_OFFSET = 8; 48 | 49 | var snapToGrid = function snapToGrid(grid, deltaX, deltaY) { 50 | var x = Math.round(deltaX / grid[0]) * grid[0]; 51 | var y = Math.round(deltaY / grid[1]) * grid[1]; 52 | return [x, y]; 53 | }; 54 | 55 | var target = { 56 | drop: function drop(props, monitor, component) { 57 | if (monitor.didDrop()) { 58 | // If you want, you can check whether some nested 59 | // target already handled drop 60 | return; 61 | } 62 | 63 | var item = monitor.getItem().item; 64 | 65 | var delta = monitor.getDifferenceFromInitialOffset(); 66 | 67 | if (!!!delta) return; 68 | 69 | if (monitor.getItemType() === _utilItemTypesJs2['default'].BOX) { 70 | var left = Math.round(isNaN(item.left) ? 0 : parseInt(item.left, 10) + delta.x); 71 | var top = Math.round(isNaN(item.top) ? 0 : parseInt(item.top, 10) + delta.y); 72 | 73 | component.moveBox(item.index, left, top); 74 | } 75 | 76 | if (monitor.getItemType() === _utilItemTypesJs2['default'].RESIZABLE_HANDLE) { 77 | var left = Math.round(delta.x < 0 ? delta.x + HANDLE_OFFSET : delta.x - HANDLE_OFFSET); 78 | var top = Math.round(delta.y < 0 ? delta.y + HANDLE_OFFSET : delta.y - HANDLE_OFFSET); 79 | component.resizeContainer(item.parent, left, top); 80 | } 81 | } 82 | }; 83 | 84 | var Container = (function (_React$Component) { 85 | _inherits(Container, _React$Component); 86 | 87 | function Container() { 88 | _classCallCheck(this, Container); 89 | 90 | _get(Object.getPrototypeOf(Container.prototype), 'constructor', this).apply(this, arguments); 91 | } 92 | 93 | _createClass(Container, [{ 94 | key: 'shouldComponentUpdate', 95 | value: function shouldComponentUpdate(nextProps, nextState) { 96 | 97 | // The comparison is fast, and we won't render the component if 98 | // it does not need it. This is a huge gain in performance. 99 | var node = this.props.node; 100 | var current = this.props.current; 101 | var update = node !== nextProps.node || (current && current.path) != (nextProps.current && nextProps.current.path); 102 | 103 | if (update) return update; 104 | 105 | //test -> container custom style changed 106 | var propsStyles = this.props.ctx.styles; 107 | var nextPropsStyles = nextProps.ctx.styles; 108 | update = propsStyles !== nextPropsStyles; 109 | 110 | return update; 111 | } 112 | }, { 113 | key: 'moveBox', 114 | value: function moveBox(index, left, top) { 115 | var deltas = snapToGrid(this.context.snapGrid, left, top); 116 | this.moveBoxEx(index, deltas[0], deltas[1]); 117 | } 118 | }, { 119 | key: 'moveBoxEx', 120 | value: function moveBoxEx(index, left, top) { 121 | var boxes = this.props.boxes; 122 | if (boxes === undefined) return; 123 | var box = boxes[index]; 124 | if (box === undefined) return; 125 | 126 | var updated = box.set({ 'style': _lodash2['default'].merge(_lodash2['default'].clone(box.style), { 'left': left, 'top': top }) }); 127 | this.props.currentChanged(updated); 128 | } 129 | 130 | // moveBoxToDirection(index, direction) { 131 | // var boxes = this.props.boxes; 132 | // if (boxes === undefined) return; 133 | // var box = boxes[index]; 134 | // if (box === undefined) return; 135 | 136 | // var deltas = this.getDirectionDeltas(direction); 137 | // var updated = box.set({ 'style': _.merge(_.clone(box.style), { 'left': (box.style.left || 0) + deltas[0], 'top': (box.style.top || 0) + deltas[1], }) }); 138 | // this.props.currentChanged(updated); 139 | // } 140 | }, { 141 | key: 'getDirectionDeltas', 142 | value: function getDirectionDeltas(direction) { 143 | var snaps = this.context.snapGrid; 144 | var deltas = [0, 0]; 145 | switch (direction) { 146 | case "left": 147 | return [-1 * snaps[0], 0]; 148 | case "right": 149 | return [snaps[0], 0]; 150 | case "up": 151 | return [0, -1 * snaps[1]]; 152 | case "down": 153 | return [0, snaps[1]]; 154 | default: 155 | return deltas; 156 | } 157 | } 158 | }, { 159 | key: 'resizeContainer', 160 | value: function resizeContainer(container, deltaWidth, deltaHeight) { 161 | if (container === undefined) return; 162 | 163 | //TODO: use merge instead of clone 164 | var style = _lodash2['default'].clone(container.style) || {}; 165 | var newWidth = (style.width || 0) + deltaWidth; 166 | if (newWidth < 0) return; 167 | var newHeight = (style.height || 0) + deltaHeight; 168 | if (newHeight < 0) return; 169 | 170 | var deltas = snapToGrid(this.context.snapGrid, newWidth, newHeight); 171 | style.width = deltas[0]; 172 | style.height = deltas[1]; 173 | 174 | var updated = container.set({ 'style': style }); 175 | this.props.currentChanged(updated); 176 | } 177 | }, { 178 | key: 'handleClick', 179 | value: function handleClick(e) { 180 | e.stopPropagation(); 181 | if (this.props.handleClick !== undefined) this.props.handleClick(); 182 | } 183 | }, { 184 | key: 'render', 185 | value: function render() { 186 | var _props = this.props; 187 | var elementName = _props.elementName; 188 | var ctx = _props.ctx; 189 | var widgets = _props.widgets; 190 | var widgetRenderer = _props.widgetRenderer; 191 | var current = _props.current; 192 | var currentChanged = _props.currentChanged; 193 | var node = _props.node; 194 | var parent = _props.parent; 195 | var dataBinder = _props.dataBinder; 196 | var _props2 = this.props; 197 | var canDrop = _props2.canDrop; 198 | var isOver = _props2.isOver; 199 | var connectDropTarget = _props2.connectDropTarget; 200 | 201 | var containers = this.props.containers || []; 202 | var boxes = this.props.boxes || []; 203 | 204 | //styles 205 | var classes = (0, _classnames2['default'])({ 206 | 'con': true, 207 | 'selected': this.props.selected, 208 | 'parentSelected': this.props.parentSelected, 209 | 'root': this.props.isRoot 210 | }); 211 | 212 | var styles = { 213 | left: this.props.left, 214 | top: this.props.top, 215 | height: this.props.height, 216 | width: this.props.width, 217 | position: this.props.position || 'relative' 218 | }; 219 | 220 | var nodeProps = node.props; 221 | var nodeBindings = node.bindings || {}; 222 | 223 | //apply custom styles 224 | var customStyle = ctx["styles"] && ctx["styles"][elementName]; 225 | if (customStyle !== undefined) nodeProps = _lodash2['default'].merge(_lodash2['default'].cloneDeep(customStyle), nodeProps); 226 | 227 | //apply node props 228 | if (dataBinder !== undefined && widgetRenderer) nodeProps = widgetRenderer.bindProps(_lodash2['default'].cloneDeep(nodeProps), nodeBindings.bindings, dataBinder, true); 229 | 230 | var containerComponent = widgets[elementName] || 'div'; 231 | 232 | return connectDropTarget(_react2['default'].createElement( 233 | 'div', 234 | { className: classes, style: styles, onClick: this.handleClick.bind(this) }, 235 | _react2['default'].createElement( 236 | 'div', 237 | null, 238 | containers.length !== 0 ? _react2['default'].createElement(containerComponent, nodeProps, containers.map(function (container, index) { 239 | 240 | var selected = container === current.node; 241 | var parentSelected = false; //container === current.parentNode; 242 | var key = container.name + index; 243 | var containerStyle = container.style || {}; 244 | 245 | var path = this.props.path + '.containers[' + index + ']'; 246 | 247 | var handleClick = function handleClick() { 248 | if (currentChanged !== undefined) currentChanged(container, path); 249 | }; 250 | 251 | var left = containerStyle.left === undefined ? 0 : parseInt(containerStyle.left, 10); 252 | var top = containerStyle.top === undefined ? 0 : parseInt(containerStyle.top, 10); 253 | 254 | var childProps = _lodash2['default'].cloneDeep(container.props) || {}; 255 | var childBindings = container.bindings || {}; 256 | 257 | //apply custom styles 258 | var childCustomStyle = ctx["styles"] && ctx["styles"][container.elementName]; 259 | if (childCustomStyle !== undefined) childProps = _lodash2['default'].merge(_lodash2['default'].cloneDeep(childCustomStyle), childProps); 260 | 261 | //apply node props 262 | if (dataBinder !== undefined && widgetRenderer) childProps = widgetRenderer.bindProps(childProps, childBindings.bindings, dataBinder, true); 263 | 264 | //specific props resolution rule -> propagate width and height from style to child container props 265 | 266 | if (!childProps.width && !!containerStyle.width) childProps.width = containerStyle.width; 267 | if (!childProps.height && !!containerStyle.height) childProps.height = containerStyle.height; 268 | if (!childProps.left && !!containerStyle.left) childProps.left = containerStyle.left; 269 | if (!childProps.top && !!containerStyle.top) childProps.top = containerStyle.top; 270 | 271 | var applyDirectChildContainers = elementName == "Grid"; //container.containers && container.containers.length === 0; 272 | //var childComponent = 'div'; 273 | var wrappedContainer = _react2['default'].createElement(WrappedContainer, { elementName: container.elementName, 274 | index: index, 275 | left: left, 276 | top: top, 277 | height: containerStyle.height, 278 | width: containerStyle.width, 279 | position: containerStyle.position || 'relative', 280 | boxes: container.boxes, 281 | containers: container.containers, 282 | node: container, 283 | path: path, 284 | parent: parent, 285 | currentChanged: currentChanged, 286 | current: current, 287 | handleClick: handleClick, 288 | parentSelected: parentSelected, 289 | selected: selected, 290 | dataBinder: dataBinder, 291 | ctx: ctx, 292 | widgets: widgets, 293 | widgetRenderer: widgetRenderer 294 | }); 295 | 296 | return applyDirectChildContainers ? _react2['default'].createElement(widgets[container.elementName] || 'div', _lodash2['default'].extend(childProps, { child: true, key: key }), wrappedContainer) : wrappedContainer; 297 | }, this)) : null, 298 | boxes.map(function (box, index) { 299 | 300 | var selected = box === current.node; 301 | var key = box.name + index; 302 | 303 | var boxStyle = box.style || {}; 304 | var left = boxStyle.left === undefined ? 0 : parseInt(box.style.left, 10); 305 | var top = boxStyle.top === undefined ? 0 : parseInt(box.style.top, 10); 306 | 307 | var path = this.props.path + '.boxes[' + index + ']'; 308 | 309 | var box = _react2['default'].createElement(_Box2['default'], { key: key, 310 | index: index, 311 | left: left, 312 | top: top, 313 | path: path, 314 | position: elementName === "Cell" ? 'relative' : 'absolute', 315 | selected: selected, 316 | hideSourceOnDrag: this.props.hideSourceOnDrag, 317 | currentChanged: currentChanged, 318 | node: box, dataBinder: dataBinder, 319 | ctx: ctx, 320 | widgets: widgets, 321 | widgetRenderer: widgetRenderer 322 | }); 323 | 324 | return box; 325 | }, this) 326 | ), 327 | this.props.isRoot || this.props.width === undefined || this.props.height === undefined ? null : _react2['default'].createElement(_ResizableHandleJs2['default'], { parent: this.props.node }) 328 | )); 329 | } 330 | }]); 331 | 332 | return Container; 333 | })(_react2['default'].Component); 334 | 335 | Container.contextTypes = { 336 | snapGrid: _propTypes2['default'].arrayOf(_propTypes2['default'].number) 337 | }; 338 | 339 | var collect = function collect(connect, monitor) { 340 | return { 341 | connectDropTarget: connect.dropTarget(), 342 | isOver: monitor.isOver(), 343 | canDrop: monitor.canDrop() 344 | }; 345 | }; 346 | var WrappedContainer = (0, _reactDnd.DropTarget)([_utilItemTypesJs2['default'].RESIZABLE_HANDLE, _utilItemTypesJs2['default'].BOX], target, collect)(Container); 347 | exports['default'] = WrappedContainer; 348 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/workplace/ResizableHandle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 16 | 17 | var _react = require('react'); 18 | 19 | var _react2 = _interopRequireDefault(_react); 20 | 21 | var _propTypes = require('prop-types'); 22 | 23 | var _propTypes2 = _interopRequireDefault(_propTypes); 24 | 25 | var _reactDnd = require('react-dnd'); 26 | 27 | var _utilItemTypesJs = require('../util/ItemTypes.js'); 28 | 29 | var _utilItemTypesJs2 = _interopRequireDefault(_utilItemTypesJs); 30 | 31 | /** 32 | * Implements the drag source contract. 33 | */ 34 | var source = { 35 | beginDrag: function beginDrag(props, monitor, component) { 36 | return { 37 | //effectAllowed: DropEffects.MOVE, 38 | item: component.props 39 | }; 40 | } 41 | }; 42 | 43 | /** 44 | * Specifies the props to inject into your component. 45 | */ 46 | function collect(connect, monitor) { 47 | return { 48 | 49 | connectDragSource: connect.dragSource(), 50 | isDragging: monitor.isDragging() 51 | }; 52 | } 53 | var propTypes = { 54 | //item: PropTypes.isRequired, 55 | 56 | // Injected by React DnD: 57 | isDragging: _propTypes2['default'].bool.isRequired, 58 | connectDragSource: _propTypes2['default'].func.isRequired 59 | }; 60 | 61 | var ResizableHandle = (function (_React$Component) { 62 | _inherits(ResizableHandle, _React$Component); 63 | 64 | function ResizableHandle() { 65 | _classCallCheck(this, ResizableHandle); 66 | 67 | _get(Object.getPrototypeOf(ResizableHandle.prototype), 'constructor', this).apply(this, arguments); 68 | } 69 | 70 | _createClass(ResizableHandle, [{ 71 | key: 'render', 72 | value: function render() { 73 | var _props = this.props; 74 | var isDragging = _props.isDragging; 75 | var connectDragSource = _props.connectDragSource; 76 | var item = _props.item; 77 | 78 | return connectDragSource(_react2['default'].createElement('div', { className: 'resizable-handle' })); 79 | } 80 | }]); 81 | 82 | return ResizableHandle; 83 | })(_react2['default'].Component); 84 | 85 | ; 86 | 87 | ResizableHandle.propTypes = propTypes; 88 | 89 | // Export the wrapped component: 90 | exports['default'] = (0, _reactDnd.DragSource)(_utilItemTypesJs2['default'].RESIZABLE_HANDLE, source, collect)(ResizableHandle); 91 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/workplace/ResizeContainer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 16 | 17 | var _react = require('react'); 18 | 19 | var _react2 = _interopRequireDefault(_react); 20 | 21 | var _propTypes = require('prop-types'); 22 | 23 | var _propTypes2 = _interopRequireDefault(_propTypes); 24 | 25 | var _utilItemTypesJs = require('../util/ItemTypes.js'); 26 | 27 | var _utilItemTypesJs2 = _interopRequireDefault(_utilItemTypesJs); 28 | 29 | var _reactDnd = require('react-dnd'); 30 | 31 | var _lodash = require('lodash'); 32 | 33 | var _lodash2 = _interopRequireDefault(_lodash); 34 | 35 | var _ResizableHandleJs = require('./ResizableHandle.js'); 36 | 37 | var _ResizableHandleJs2 = _interopRequireDefault(_ResizableHandleJs); 38 | 39 | var target = { 40 | drop: function drop(props, monitor, component) { 41 | if (monitor.didDrop()) { 42 | // If you want, you can check whether some nested 43 | // target already handled drop 44 | return; 45 | } 46 | 47 | var item = monitor.getItem().item; 48 | 49 | var delta = monitor.getDifferenceFromInitialOffset(); 50 | 51 | if (!!!delta) return; 52 | 53 | if (monitor.getItemType() === _utilItemTypesJs2['default'].RESIZABLE_HANDLE) { 54 | var left = Math.round(delta.x); 55 | var top = Math.round(delta.y); 56 | 57 | component.resizeContainer(item.parent, left, top); 58 | }; 59 | } 60 | }; 61 | var HANDLE_OFFSET = 30; 62 | 63 | var ResizeContainer = (function (_React$Component) { 64 | _inherits(ResizeContainer, _React$Component); 65 | 66 | function ResizeContainer() { 67 | _classCallCheck(this, ResizeContainer); 68 | 69 | _get(Object.getPrototypeOf(ResizeContainer.prototype), 'constructor', this).apply(this, arguments); 70 | } 71 | 72 | _createClass(ResizeContainer, [{ 73 | key: 'resizeContainer', 74 | value: function resizeContainer(container, deltaWidth, deltaHeight) { 75 | if (container === undefined) return; 76 | 77 | //TODO: use merge instead of clone 78 | var style = _lodash2['default'].cloneDeep(container.style); 79 | style.width += deltaWidth; 80 | style.height += deltaHeight; 81 | 82 | var updated = container.set({ 'style': style }); 83 | this.props.currentChanged(updated); 84 | } 85 | }, { 86 | key: 'render', 87 | value: function render() { 88 | var _props = this.props; 89 | var canDrop = _props.canDrop; 90 | var isOver = _props.isOver; 91 | var connectDropTarget = _props.connectDropTarget; 92 | 93 | var style = this.props.node && this.props.node.style || {}; 94 | 95 | //resize handle position 96 | var useResize = !!style.height && !!style.width; 97 | var resizeHandlePosition = { top: style.height - HANDLE_OFFSET, left: style.width - HANDLE_OFFSET }; 98 | 99 | return connectDropTarget(_react2['default'].createElement( 100 | 'div', 101 | null, 102 | this.props.children, 103 | useResize ? _react2['default'].createElement(_ResizableHandleJs2['default'], { styles: style, left: resizeHandlePosition.left, top: resizeHandlePosition.top, parent: this.props.node }) : null 104 | )); 105 | } 106 | }]); 107 | 108 | return ResizeContainer; 109 | })(_react2['default'].Component); 110 | 111 | ; 112 | 113 | var collect = function collect(connect, monitor) { 114 | return { 115 | connectDropTarget: connect.dropTarget(), 116 | isOver: monitor.isOver(), 117 | canDrop: monitor.canDrop() 118 | }; 119 | }; 120 | 121 | // Export the wrapped component: 122 | exports['default'] = (0, _reactDnd.DropTarget)(_utilItemTypesJs2['default'].RESIZABLE_HANDLE, target, collect)(ResizeContainer); 123 | module.exports = exports['default']; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-designer-core", 3 | "version": "1.4.1", 4 | "description": "React-designer-core is a set of core components for easy content creation.", 5 | "main": "lib/Designer.js", 6 | "author": "Roman Samec", 7 | "homepage": "https://github.com/rsamec/react-designer-core", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/rsamec/react-designer-core.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/rsamec/react-designer-core/issues" 14 | }, 15 | "dependencies": { 16 | "classnames": "^2.2.3", 17 | "lodash": "^4.17.4", 18 | "prop-types": "^15.6.0", 19 | "react-dnd": "^2.5.3", 20 | "react-dnd-html5-backend": "^2.2.0", 21 | "react-treeview": "^0.4.7", 22 | "transhand": "^0.1.26" 23 | }, 24 | "devDependencies": { 25 | "babel-eslint": "^4.1.3", 26 | "babel-runtime": "^6.6.1", 27 | "babelify": "^7.3.0", 28 | "draft-js": "^0.7.0", 29 | "draft-js-export-html": "^0.2.2", 30 | "eslint": "^1.6.0", 31 | "eslint-plugin-react": "^3.5.1", 32 | "freezer-js": "^0.13.0", 33 | "gulp": "^3.9.0", 34 | "gu": "^0.7.6", 35 | "react-overlays": "^0.8.1", 36 | "react-split-pane": "^0.1.66" 37 | }, 38 | "peerDependencies": { 39 | "react": ">=15.0.0", 40 | "react-dom": ">=15.0.0" 41 | }, 42 | "browserify-shim": { 43 | "react": "global:React" 44 | }, 45 | "scripts": { 46 | "build": "gulp clean && NODE_ENV=production gulp build", 47 | "examples": "gulp dev:server", 48 | "lint": "eslint ./; true", 49 | "publish:site": "NODE_ENV=production gulp publish:examples", 50 | "release": "NODE_ENV=production gulp release", 51 | "start": "gulp dev", 52 | "test": "echo \"no tests yet\" && exit 0", 53 | "watch": "gulp watch:lib" 54 | }, 55 | "keywords": [ 56 | "react", 57 | "react-component" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/Designer.js: -------------------------------------------------------------------------------- 1 | import Workplace from './components/Workplace.js'; 2 | import ObjectBrowser from './components/ObjectBrowser.js'; 3 | 4 | export default { 5 | Workplace:Workplace, 6 | ObjectBrowser:ObjectBrowser 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/ObjectBrowser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeView from 'react-treeview'; 3 | import _ from 'lodash'; 4 | import cx from 'classnames'; 5 | 6 | const CONTAINER_KEYS = ["ObjectSchema","Container","Repeater","Grid","Cell","BackgroundContainer"]; 7 | 8 | export default class ObjectBrowser extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = {filterText: ''}; 12 | } 13 | handleUserInput(e) { 14 | this.setState({ 15 | filterText: e.target.value 16 | }); 17 | } 18 | executeAction(action,args){ 19 | if (action === "onDragStart"){ 20 | this.currentItem = args; 21 | return; 22 | } 23 | if (action === "onDrop"){ 24 | this.move(this.currentItem,args); 25 | this.currentItem = undefined; 26 | return; 27 | } 28 | } 29 | move(from, to){ 30 | //console.log(from.node.name + " -> " + to.node.name); 31 | // transact returns a mutable object 32 | // to make all the local changes 33 | 34 | //find source 35 | var source = from.node; 36 | var isContainer = _.includes(CONTAINER_KEYS,source.elementName); 37 | 38 | //move source to target - do it in transaction 39 | var targetArray = isContainer? to.node.containers:to.node.boxes; 40 | targetArray.transact().push(from.node); 41 | 42 | //remove source - do it in transaction 43 | var sourceParent = isContainer?from.parentNode.containers:from.parentNode.boxes; 44 | var indexToRemove = sourceParent.indexOf(source); 45 | //console.log(indexToRemove); 46 | if (indexToRemove !== -1) { 47 | sourceParent.transact().splice(indexToRemove,1); 48 | } 49 | 50 | // all the changes are made at once 51 | targetArray.run(); 52 | sourceParent.run(); 53 | 54 | // use it as a normal array 55 | //trans[0] = 1000; // [1000, 1, 2, ..., 999] 56 | } 57 | render() { 58 | var classes = cx({ 59 | 'node': true, 60 | 'selected': this.props.current.node === this.props.rootNode 61 | }); 62 | let path = 'schema'; 63 | return ( 64 |
65 |
66 | 67 |
68 |
this.props.currentChanged(this.props.rootNode,path)}>{this.props.rootNode.name}
69 | {this.props.rootNode.containers.length === 0 ? No objects to show. : 70 | 73 | } 74 |
75 | ); 76 | } 77 | }; 78 | 79 | class TreeNode extends React.Component 80 | { 81 | handleClick(node,path) { 82 | this.props.currentChanged(node,path); 83 | } 84 | 85 | //TODO: optimize -> now each node starts its own tree traversal 86 | hideNode(node,filterText){ 87 | var trav = function(node){ 88 | var containers = node.containers; 89 | var boxes = node.boxes; 90 | var anyBoxes = _.some(boxes,function(item){return item.name.toLowerCase().indexOf(filterText.toLowerCase()) !== -1}); 91 | if (anyBoxes) return true; 92 | if (node.name.indexOf(filterText) !== -1) return true; 93 | //recursion condtion stop 94 | var childrenBoxes = false; 95 | for (var i in containers) 96 | { 97 | //recursion step 98 | childrenBoxes = trav(containers[i]); 99 | if (childrenBoxes) return true; 100 | } 101 | return false; 102 | }; 103 | 104 | return !trav(node); 105 | } 106 | shouldComponentUpdate( nextProps ){ 107 | return true; 108 | // The comparison is fast, and we won't render the component if 109 | // it does not need it. This is a huge gain in performance. 110 | //var current = this.props.current.node; 111 | //var nextCurrent = nextProps.current.node; 112 | //return this.props.filterText != nextProps.filterText || this.props.nodes != nextProps.nodes || (current!==undefined && nextCurrent !==undefined && current.name != nextCurrent.name); 113 | } 114 | render() { 115 | var containers = this.props.node.containers || []; 116 | 117 | return ( 118 |
119 | {containers.map(function (node, i) { 120 | if (this.hideNode(node,this.props.filterText)) return; 121 | 122 | var onDragStart = function(e) { 123 | console.log('drag started'); 124 | e.stopPropagation(); 125 | var draggingItem = { 126 | node: node, 127 | parentNode : this.props.node}; 128 | this.props.executeAction("onDragStart",draggingItem); 129 | 130 | }.bind(this); 131 | var onDragEnter = function(e){ 132 | e.preventDefault(); // Necessary. Allows us to drop. 133 | e.stopPropagation(); 134 | //window.event.returnValue=false; 135 | //if (this.props.dragging.type !== this.props.item.type || this.props.dragging.id !== this.props.item.id) { 136 | var dropCandidate = {node: node, parentNode:this.props.node}; 137 | // var self = this; 138 | this.props.executeAction("dropPossible", dropCandidate); 139 | //} 140 | }.bind(this); 141 | var onDragOver = function(e){ 142 | e.preventDefault(); // Necessary. Allows us to drop. 143 | e.stopPropagation(); 144 | //window.event.returnValue=false; 145 | }.bind(this); 146 | var onDrop = function(e) { 147 | e.preventDefault(); 148 | e.stopPropagation(); 149 | var dropPlace= {node: node, parentNode: this.props.node}; 150 | this.props.executeAction("onDrop",dropPlace); 151 | }.bind(this); 152 | 153 | 154 | var type = node.elementName; 155 | 156 | var containers = node.containers || []; 157 | var boxes = node.boxes || []; 158 | 159 | var selected = this.props.current.node === node; 160 | var parentSelected = this.props.current.parentNode === node; 161 | var path = `${this.props.path}.containers[${i}]`; 162 | 163 | var classes = cx({ 164 | 'node': true, 165 | 'selected': selected, 166 | 'parentSelected':this.props.parentSelected 167 | }); 168 | 169 | var label = {node.name}; 172 | return ( 173 | 174 | 175 | 176 | {boxes.map(function (box, j) { 177 | 178 | var onDragStart1 = function(e) { 179 | console.log('drag started'); 180 | e.stopPropagation(); 181 | var draggingItem = { 182 | node: box, 183 | parentNode : node}; 184 | this.props.executeAction("onDragStart",draggingItem); 185 | 186 | }.bind(this); 187 | 188 | if (box.name.toLowerCase().indexOf(this.props.filterText.toLowerCase()) === -1) { 189 | return; 190 | } 191 | 192 | var boxPath = `${path}.boxes[${j}]`; 193 | var classes = cx({ 194 | 'node': true, 195 | 'selected': this.props.current.node === box 196 | }); 197 | return (
{box.name}
) 198 | 199 | },this)} 200 | 201 |
202 | ); 203 | }, this)} 204 |
205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/components/Workplace.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | import PropTypes from 'prop-types'; 3 | import _ from 'lodash'; 4 | import HTML5Backend from 'react-dnd-html5-backend'; 5 | import { DragDropContext } from 'react-dnd'; 6 | import {CSSTranshand} from 'transhand'; 7 | import Container from './../workplace/Container'; 8 | 9 | import backgroundStyle from '../util/backgroundStyle'; 10 | 11 | const DEFAULT_TRANSFORM = { 12 | tx: 0, ty: 0, //translate in px 13 | sx: 1, sy: 1, //scale 14 | rz: 0, //rotation in radian 15 | ox: 0.5, oy: 0.5 //transform origin 16 | }; 17 | 18 | const DEFAULT_ROOT_PATH = 'path'; 19 | 20 | class Workplace extends React.Component { 21 | 22 | constructor(props){ 23 | super(props); 24 | this.state = {}; 25 | } 26 | getChildContext() { 27 | return {snapGrid: this.props.snapGrid}; 28 | } 29 | handleChange(change) { 30 | var currentNode = this.props.current.node; 31 | if (currentNode == undefined) return; 32 | 33 | var style = currentNode.style; 34 | if (style === undefined) return; 35 | 36 | //resolution strategy -> defaultTransform -> style.transform -> change 37 | var transform = _.merge(_.merge(_.clone(DEFAULT_TRANSFORM), style.transform), change); 38 | 39 | 40 | var updated = currentNode.set('style', _.extend(_.clone(style), {'transform': transform})); 41 | this.props.currentChanged(updated); 42 | } 43 | 44 | 45 | currentChanged(node,path,domEl) { 46 | if (this.props.currentChanged !== undefined) this.props.currentChanged(node,path); 47 | this.setState({ 48 | currentDOMNode: domEl 49 | }); 50 | } 51 | 52 | render() { 53 | 54 | const {schema,current,currentChanged, dataContext} = this.props; 55 | 56 | var handleClick = function () { 57 | if (currentChanged !== undefined) currentChanged(schema,DEFAULT_ROOT_PATH); 58 | }; 59 | 60 | 61 | 62 | var style = current.node && current.node.style || {}; 63 | var transform = _.merge(_.clone(DEFAULT_TRANSFORM), style.transform); 64 | 65 | var ctx = (schema.props && schema.props.context) || {}; 66 | var customStyles = ctx['styles'] || {}; 67 | var code = ctx['code'] && ctx['code'].compiled; 68 | var customCode = !!code ? eval(code) : undefined; 69 | 70 | //append shared code to data context 71 | if (dataContext !== undefined) dataContext.customCode = customCode; 72 | 73 | var context = { 74 | styles: customStyles, 75 | customCode: customCode 76 | }; 77 | 78 | 79 | var bg = (schema.props && schema.props.background) || {}; 80 | var bgStyle = backgroundStyle(bg); 81 | 82 | bgStyle.position = 'absolute'; 83 | bgStyle.width = '100%'; 84 | bgStyle.height = '100%'; 85 | bgStyle.zIndex = -1; 86 | 87 | var component = 88 | ; 102 | 103 | return (
104 |
105 | {component} 106 | {this.state.currentDOMNode !== undefined ? 107 | : null} 109 | 110 |
); 111 | } 112 | }; 113 | Workplace.childContextTypes = {snapGrid: PropTypes.arrayOf(PropTypes.number)}; 114 | export default DragDropContext(HTML5Backend)(Workplace); 115 | -------------------------------------------------------------------------------- /src/util/ItemTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | BLOCK: 'block', 3 | IMAGE: 'image', 4 | BOX:'box', 5 | CONTAINER:'container', 6 | RESIZABLE_HANDLE:'resize', 7 | TREE_NODE:'treeNode' 8 | }; 9 | -------------------------------------------------------------------------------- /src/util/backgroundStyle.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default function(source,panelSize) { 4 | var bg = source; 5 | var bgStyle = {}; 6 | if (bg === undefined) return bgStyle; 7 | 8 | //size 9 | if (!!bg.size) { 10 | if (panelSize !== undefined && (bg.size === "leftHalf" || bg.size === "rightHalf")) { 11 | bgStyle.backgroundSize = `${panelSize.width * 2}px ${panelSize.height}px`; 12 | bgStyle.backgroundPosition = bg.size === "leftHalf" ? '0% 0%' : '100% 0%'; 13 | //console.log(bgStyle); 14 | } 15 | else{ 16 | bgStyle.backgroundSize = bg.size; 17 | if (!!bg.position) bgStyle.backgroundPosition = bg.position; 18 | } 19 | } 20 | //gradient 21 | var bgGradient = bg.gradient; 22 | if (bgGradient !== undefined) { 23 | //gradient 24 | if (bgGradient.stops !== undefined) { 25 | var gradientStops = _.reduce(bgGradient.stops, function (memo, stop) { 26 | return memo + ', ' + stop.color + ' ' + (stop.offset * 100) + '%' 27 | }, ''); 28 | 29 | var orientation = bgGradient.orientation || 'top'; 30 | var grandientType = bgGradient.orientation === 'center, ellipse cover' ? '-webkit-radial-gradient' : '-webkit-linear-gradient'; 31 | bgStyle.background = grandientType + '(' + orientation + gradientStops + ')'; 32 | } 33 | } 34 | 35 | //color 36 | var bgColor = bg.color; 37 | if (bgColor !== undefined) { 38 | if (!!bgColor.color) bgStyle.backgroundColor = bgColor.color; 39 | if (!!bgColor.alpha) bgStyle.opacity = bgColor.alpha / 100; 40 | } 41 | 42 | if (!!bg.image) bgStyle.backgroundImage = 'url(' + bg.image + ')'; 43 | if (!!bg.repeat) bgStyle.backgroundRepeat = bg.repeat; 44 | if (!!bg.attachment) bgStyle.backgroundAttachment = bg.attachment; 45 | 46 | var filter = bg.filter || {}; 47 | var cssFilter = ""; 48 | if (!!filter.blur) cssFilter += ' blur(' + filter.blur + 'px)'; 49 | if (!!filter.brightness) cssFilter += ' brightness(' + filter.brightness + '%)'; 50 | if (!!filter.contrast) cssFilter += ' contrast(' + filter.contrast + '%)'; 51 | if (!!filter.grayscale) cssFilter += ' grayscale(' + filter.grayscale + '%)'; 52 | if (!!filter.hueRotate) cssFilter += ' hue-rotate(' + filter.hueRotate + 'deg)'; 53 | if (!!filter.invert) cssFilter += ' invert(' + filter.invert + '%)'; 54 | if (!!filter.opacity) cssFilter += ' opacity(' + filter.opacity + '%)'; 55 | if (!!filter.saturate) cssFilter += ' saturate(' + filter.saturate + '%)'; 56 | if (!!filter.sepia) cssFilter += ' sepia(' + filter.sepia + '%)'; 57 | 58 | if (!!cssFilter) { 59 | bgStyle.WebkitFilter = cssFilter; 60 | bgStyle.filter = cssFilter; 61 | } 62 | //bgStyle.position = 'absolute'; 63 | return bgStyle; 64 | } 65 | -------------------------------------------------------------------------------- /src/util/generateCssTransform.js: -------------------------------------------------------------------------------- 1 | export default function generateCssTransform(transform) { 2 | var cssTransform = ''; 3 | 4 | if (transform.tx !== undefined) cssTransform += ' translateX(' + transform.tx + 'px)'; 5 | if (transform.ty !== undefined) cssTransform += ' translateY(' + transform.ty + 'px)'; 6 | if (transform.rz !== undefined) cssTransform += ' rotate(' + transform.rz + 'rad)'; 7 | if (transform.sx !== undefined) cssTransform += ' scaleX(' + transform.sx + ')'; 8 | if (transform.sy !== undefined) cssTransform += ' scaleY(' + transform.sy + ')'; 9 | 10 | return cssTransform 11 | } 12 | -------------------------------------------------------------------------------- /src/workplace/Box.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactDOM from 'react-dom'; 4 | import { DragSource } from 'react-dnd'; 5 | import _ from 'lodash'; 6 | import cx from 'classnames'; 7 | 8 | import ResizeContainer from './ResizeContainer.js'; 9 | 10 | import ItemTypes from '../util/ItemTypes.js'; 11 | import generateCssTransform from '../util/generateCssTransform'; 12 | 13 | /** 14 | * Implements the drag source contract. 15 | */ 16 | const source = { 17 | beginDrag(props, monitor, component) { 18 | return { 19 | //effectAllowed: DropEffects.MOVE, 20 | item: component.props 21 | }; 22 | } 23 | }; 24 | 25 | /** 26 | * Specifies the props to inject into your component. 27 | */ 28 | function collect(connect, monitor) { 29 | return { 30 | connectDragSource: connect.dragSource(), 31 | isDragging: monitor.isDragging() 32 | }; 33 | } 34 | 35 | const propTypes = { 36 | // Injected by React DnD: 37 | isDragging: PropTypes.bool.isRequired, 38 | connectDragSource: PropTypes.func.isRequired 39 | }; 40 | 41 | class Box extends React.Component { 42 | 43 | 44 | handleDoubleClick(e) { 45 | e.stopPropagation(); 46 | if (this.props.currentChanged !== undefined) this.props.currentChanged(this.props.node, this.props.path, ReactDOM.findDOMNode(this)); 47 | } 48 | 49 | handleClick(e) { 50 | e.stopPropagation(); 51 | if (this.props.currentChanged !== undefined) this.props.currentChanged(this.props.node,this.props.path); 52 | } 53 | 54 | shouldComponentUpdate(nextProps) { 55 | 56 | // The comparison is fast, and we won't render the component if 57 | // it does not need it. This is a huge gain in performance. 58 | var box = this.props.node; 59 | var update = box !== nextProps.node || this.props.selected != nextProps.selected; 60 | //console.log(nextProps.node.name + ' : ' + update); 61 | if (update) return update; 62 | 63 | //test -> widget custom style changed 64 | var propsStyles = this.props.ctx.styles; 65 | var nextPropsStyles = nextProps.ctx.styles; 66 | update = (propsStyles && propsStyles[box.elementName]) !== (nextPropsStyles && nextPropsStyles[box.elementName]); 67 | 68 | return update; 69 | } 70 | // componentWillReceiveProps(nextProps){ 71 | // var index = this.props.index; 72 | // for (var action of ['right','left','up','down']) 73 | // { 74 | // if (nextProps.selected)this.props.bindShortcut(action, this.props.moveBox.bind(this,index,action));//: this.props.unbindShortcut(action); 75 | // } 76 | // } 77 | render() { 78 | 79 | const {widgets,widgetRenderer,selected,node,dataBinder, currentChanged, index} = this.props; 80 | const {isDragging, connectDragSource, item } = this.props; 81 | 82 | //prepare styles 83 | var classes = cx({ 84 | 'box': true, 85 | 'selected': selected 86 | }); 87 | 88 | //clone node 89 | var box = node.toJS(); 90 | 91 | //document custom style 92 | var ctx = this.props.ctx || {}; 93 | var customStyle = ctx["styles"] && ctx["styles"][box.elementName]; 94 | 95 | //specific props resolution rule -> propagate width and height from style to widget props 96 | var boxProps = box.props || {}; 97 | var boxStyle = box.style || {}; 98 | if (!boxProps.width && !!boxStyle.width) boxProps.width =boxStyle.width; 99 | if (!boxProps.height && !!boxStyle.height) boxProps.height = boxStyle.height; 100 | 101 | 102 | var boxComponent = widgetRenderer !== undefined && widgets !== undefined ? 103 | React.createElement(widgetRenderer,{ 104 | tabIndex: index, 105 | widget:widgets[box.elementName], 106 | node:box, 107 | dataBinder:dataBinder, 108 | customStyle:customStyle, 109 | customCode:ctx['customCode'], 110 | designer:true, 111 | current:node, 112 | currentChanged:currentChanged, 113 | selected:selected 114 | },null) : 115 |
No widget renderer or widget factory provided.
; 116 | 117 | 118 | //create style 119 | var styles = _.extend({position:this.props.position},boxStyle); 120 | if (boxStyle.transform !== undefined) styles['transform'] = generateCssTransform(boxStyle.transform); 121 | 122 | //wrap with div double click for transhand transformation 123 | if (box.elementName !== "Core.RichTextContent") boxComponent =
{boxComponent}
; 124 | 125 | return connectDragSource( 126 |
127 | 128 | {boxComponent} 129 | 130 |
131 | ); 132 | } 133 | }; 134 | 135 | Box.propTypes = propTypes; 136 | // Export the wrapped component: 137 | export default DragSource(ItemTypes.BOX, source, collect)(Box); 138 | -------------------------------------------------------------------------------- /src/workplace/Container.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ItemTypes from '../util/ItemTypes.js'; 4 | import { DropTarget } from 'react-dnd'; 5 | 6 | 7 | import _ from 'lodash'; 8 | import cx from 'classnames'; 9 | 10 | import Box from './Box'; 11 | import ResizableHandle from './ResizableHandle.js'; 12 | 13 | const HANDLE_OFFSET = 8; 14 | 15 | let snapToGrid = function (grid, deltaX, deltaY) { 16 | let x = Math.round(deltaX / grid[0]) * grid[0]; 17 | let y = Math.round(deltaY / grid[1]) * grid[1]; 18 | return [x, y]; 19 | } 20 | 21 | 22 | const target = { 23 | drop(props, monitor, component) { 24 | if (monitor.didDrop()) { 25 | // If you want, you can check whether some nested 26 | // target already handled drop 27 | return; 28 | } 29 | 30 | var item = monitor.getItem().item; 31 | 32 | var delta = monitor.getDifferenceFromInitialOffset(); 33 | 34 | if (!!!delta) return; 35 | 36 | if (monitor.getItemType() === ItemTypes.BOX) { 37 | var left = Math.round(isNaN(item.left) ? 0 : parseInt(item.left, 10) + delta.x); 38 | var top = Math.round(isNaN(item.top) ? 0 : parseInt(item.top, 10) + delta.y); 39 | 40 | component.moveBox(item.index, left, top); 41 | } 42 | 43 | if (monitor.getItemType() === ItemTypes.RESIZABLE_HANDLE) { 44 | var left = Math.round(delta.x < 0 ? delta.x + HANDLE_OFFSET : delta.x - HANDLE_OFFSET); 45 | var top = Math.round(delta.y < 0 ? delta.y + HANDLE_OFFSET : delta.y - HANDLE_OFFSET); 46 | component.resizeContainer(item.parent, left, top); 47 | } 48 | } 49 | }; 50 | 51 | class Container extends React.Component { 52 | shouldComponentUpdate(nextProps, nextState) { 53 | 54 | // The comparison is fast, and we won't render the component if 55 | // it does not need it. This is a huge gain in performance. 56 | var node = this.props.node; 57 | var current = this.props.current; 58 | var update = node !== nextProps.node || (current && current.path) != (nextProps.current && nextProps.current.path); 59 | 60 | if (update) return update; 61 | 62 | //test -> container custom style changed 63 | var propsStyles = this.props.ctx.styles; 64 | var nextPropsStyles = nextProps.ctx.styles; 65 | update = (propsStyles !== nextPropsStyles); 66 | 67 | return update; 68 | } 69 | 70 | moveBox(index, left, top) { 71 | var deltas = snapToGrid(this.context.snapGrid, left, top); 72 | this.moveBoxEx(index, deltas[0], deltas[1]); 73 | } 74 | moveBoxEx(index, left, top) { 75 | var boxes = this.props.boxes; 76 | if (boxes === undefined) return; 77 | var box = boxes[index]; 78 | if (box === undefined) return; 79 | 80 | var updated = box.set({ 'style': _.merge(_.clone(box.style), { 'left': left, 'top': top }) }); 81 | this.props.currentChanged(updated); 82 | } 83 | // moveBoxToDirection(index, direction) { 84 | // var boxes = this.props.boxes; 85 | // if (boxes === undefined) return; 86 | // var box = boxes[index]; 87 | // if (box === undefined) return; 88 | 89 | // var deltas = this.getDirectionDeltas(direction); 90 | // var updated = box.set({ 'style': _.merge(_.clone(box.style), { 'left': (box.style.left || 0) + deltas[0], 'top': (box.style.top || 0) + deltas[1], }) }); 91 | // this.props.currentChanged(updated); 92 | // } 93 | getDirectionDeltas(direction) { 94 | var snaps = this.context.snapGrid; 95 | var deltas = [0, 0] 96 | switch (direction) { 97 | case "left": 98 | return [-1 * snaps[0], 0]; 99 | case "right": 100 | return [snaps[0], 0]; 101 | case "up": 102 | return [0, -1 * snaps[1]]; 103 | case "down": 104 | return [0, snaps[1]]; 105 | default: 106 | return deltas; 107 | } 108 | } 109 | 110 | resizeContainer(container, deltaWidth, deltaHeight) { 111 | if (container === undefined) return; 112 | 113 | //TODO: use merge instead of clone 114 | var style = _.clone(container.style) || {}; 115 | var newWidth = (style.width || 0) + deltaWidth; 116 | if (newWidth < 0) return; 117 | var newHeight = (style.height || 0) + deltaHeight; 118 | if (newHeight < 0) return; 119 | 120 | var deltas = snapToGrid(this.context.snapGrid, newWidth, newHeight); 121 | style.width = deltas[0]; 122 | style.height = deltas[1]; 123 | 124 | var updated = container.set({ 'style': style }); 125 | this.props.currentChanged(updated); 126 | 127 | } 128 | 129 | handleClick(e) { 130 | e.stopPropagation(); 131 | if (this.props.handleClick !== undefined) this.props.handleClick(); 132 | } 133 | 134 | render() { 135 | let { elementName, ctx, widgets, widgetRenderer, current, currentChanged, node, parent, dataBinder } = this.props; 136 | const { canDrop, isOver, connectDropTarget } = this.props; 137 | 138 | var containers = this.props.containers || []; 139 | var boxes = this.props.boxes || []; 140 | 141 | //styles 142 | var classes = cx({ 143 | 'con': true, 144 | 'selected': this.props.selected, 145 | 'parentSelected': this.props.parentSelected, 146 | 'root': this.props.isRoot 147 | }); 148 | 149 | var styles = { 150 | left: this.props.left, 151 | top: this.props.top, 152 | height: this.props.height, 153 | width: this.props.width, 154 | position: this.props.position || 'relative' 155 | }; 156 | 157 | 158 | var nodeProps = node.props; 159 | var nodeBindings = node.bindings || {}; 160 | 161 | //apply custom styles 162 | var customStyle = ctx["styles"] && ctx["styles"][elementName]; 163 | if (customStyle !== undefined) nodeProps = _.merge(_.cloneDeep(customStyle), nodeProps); 164 | 165 | //apply node props 166 | if (dataBinder !== undefined && widgetRenderer) nodeProps = widgetRenderer.bindProps(_.cloneDeep(nodeProps), nodeBindings.bindings, dataBinder, true); 167 | 168 | 169 | var containerComponent = widgets[elementName] || 'div'; 170 | 171 | return connectDropTarget( 172 |
173 |
174 | {containers.length !== 0 ? React.createElement(containerComponent, nodeProps, containers.map(function (container, index) { 175 | 176 | var selected = container === current.node; 177 | var parentSelected = false; //container === current.parentNode; 178 | var key = container.name + index; 179 | var containerStyle = container.style || {}; 180 | 181 | var path = `${this.props.path}.containers[${index}]`; 182 | 183 | var handleClick = function () { 184 | if (currentChanged !== undefined) currentChanged(container, path); 185 | } 186 | 187 | var left = containerStyle.left === undefined ? 0 : parseInt(containerStyle.left, 10); 188 | var top = containerStyle.top === undefined ? 0 : parseInt(containerStyle.top, 10); 189 | 190 | 191 | var childProps = _.cloneDeep(container.props) || {}; 192 | var childBindings = container.bindings || {}; 193 | 194 | //apply custom styles 195 | var childCustomStyle = ctx["styles"] && ctx["styles"][container.elementName]; 196 | if (childCustomStyle !== undefined) childProps = _.merge(_.cloneDeep(childCustomStyle), childProps); 197 | 198 | //apply node props 199 | if (dataBinder !== undefined && widgetRenderer) childProps = widgetRenderer.bindProps(childProps, childBindings.bindings, dataBinder, true); 200 | 201 | 202 | //specific props resolution rule -> propagate width and height from style to child container props 203 | 204 | if (!childProps.width && !!containerStyle.width) childProps.width = containerStyle.width; 205 | if (!childProps.height && !!containerStyle.height) childProps.height = containerStyle.height; 206 | if (!childProps.left && !!containerStyle.left) childProps.left = containerStyle.left; 207 | if (!childProps.top && !!containerStyle.top) childProps.top = containerStyle.top; 208 | 209 | var applyDirectChildContainers = elementName == "Grid";//container.containers && container.containers.length === 0; 210 | //var childComponent = 'div'; 211 | let wrappedContainer = ; 233 | 234 | return applyDirectChildContainers ? (React.createElement(widgets[container.elementName] || 'div', _.extend(childProps, { child: true, key: key }), wrappedContainer)) : wrappedContainer; 235 | 236 | }, this)) : null} 237 | 238 | {boxes.map(function (box, index) { 239 | 240 | var selected = box === current.node; 241 | var key = box.name + index; 242 | 243 | var boxStyle = box.style || {}; 244 | var left = boxStyle.left === undefined ? 0 : parseInt(box.style.left, 10); 245 | var top = boxStyle.top === undefined ? 0 : parseInt(box.style.top, 10); 246 | 247 | var path = `${this.props.path}.boxes[${index}]`; 248 | 249 | var box = 263 | 264 | 265 | return box; 266 | }, this) 267 | } 268 |
269 | {this.props.isRoot || (this.props.width === undefined || this.props.height === undefined) ? null : 270 | 271 | } 272 |
273 | ); 274 | } 275 | } 276 | 277 | Container.contextTypes = { 278 | snapGrid: PropTypes.arrayOf(PropTypes.number) 279 | } 280 | 281 | var collect = (connect, monitor) => ({ 282 | connectDropTarget: connect.dropTarget(), 283 | isOver: monitor.isOver(), 284 | canDrop: monitor.canDrop() 285 | }); 286 | var WrappedContainer = DropTarget([ItemTypes.RESIZABLE_HANDLE, ItemTypes.BOX], target, collect)(Container); 287 | export default WrappedContainer; 288 | -------------------------------------------------------------------------------- /src/workplace/ResizableHandle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DragSource } from 'react-dnd'; 4 | import ItemTypes from '../util/ItemTypes.js'; 5 | 6 | /** 7 | * Implements the drag source contract. 8 | */ 9 | const source = { 10 | beginDrag(props, monitor, component) { 11 | return { 12 | //effectAllowed: DropEffects.MOVE, 13 | item: component.props 14 | }; 15 | } 16 | }; 17 | 18 | /** 19 | * Specifies the props to inject into your component. 20 | */ 21 | function collect(connect, monitor) { 22 | return { 23 | 24 | connectDragSource: connect.dragSource(), 25 | isDragging: monitor.isDragging() 26 | }; 27 | } 28 | const propTypes = { 29 | //item: PropTypes.isRequired, 30 | 31 | 32 | // Injected by React DnD: 33 | isDragging: PropTypes.bool.isRequired, 34 | connectDragSource: PropTypes.func.isRequired 35 | }; 36 | 37 | class ResizableHandle extends React.Component { 38 | render() { 39 | const { isDragging, connectDragSource, item } = this.props; 40 | return connectDragSource( 41 |
42 |
43 | ); 44 | } 45 | }; 46 | 47 | ResizableHandle.propTypes = propTypes; 48 | 49 | // Export the wrapped component: 50 | export default DragSource(ItemTypes.RESIZABLE_HANDLE, source, collect)(ResizableHandle); 51 | 52 | -------------------------------------------------------------------------------- /src/workplace/ResizeContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ItemTypes from '../util/ItemTypes.js'; 4 | import { DropTarget } from 'react-dnd'; 5 | 6 | import _ from 'lodash'; 7 | import ResizableHandle from './ResizableHandle.js'; 8 | 9 | 10 | const target = { 11 | drop(props, monitor, component) { 12 | if (monitor.didDrop()) { 13 | // If you want, you can check whether some nested 14 | // target already handled drop 15 | return; 16 | } 17 | 18 | var item = monitor.getItem().item; 19 | 20 | var delta = monitor.getDifferenceFromInitialOffset(); 21 | 22 | if (!!!delta) return; 23 | 24 | if (monitor.getItemType() === ItemTypes.RESIZABLE_HANDLE) { 25 | var left = Math.round(delta.x); 26 | var top = Math.round(delta.y); 27 | 28 | component.resizeContainer(item.parent, left, top); 29 | }; 30 | 31 | } 32 | }; 33 | const HANDLE_OFFSET = 30; 34 | 35 | class ResizeContainer extends React.Component { 36 | resizeContainer(container, deltaWidth, deltaHeight) { 37 | if (container === undefined) return; 38 | 39 | //TODO: use merge instead of clone 40 | var style = _.cloneDeep(container.style); 41 | style.width += deltaWidth; 42 | style.height += deltaHeight; 43 | 44 | var updated = container.set({'style': style}); 45 | this.props.currentChanged(updated); 46 | 47 | } 48 | render() { 49 | 50 | const { canDrop, isOver, connectDropTarget} = this.props; 51 | 52 | var style = this.props.node && this.props.node.style || {}; 53 | 54 | //resize handle position 55 | var useResize = !!style.height && !!style.width; 56 | var resizeHandlePosition = {top: (style.height - HANDLE_OFFSET), left: (style.width - HANDLE_OFFSET)}; 57 | 58 | 59 | return connectDropTarget( 60 |
61 | {this.props.children} 62 | {useResize? 63 | :null 64 | } 65 |
66 | ); 67 | } 68 | }; 69 | 70 | var collect = (connect, monitor) => ({ 71 | connectDropTarget: connect.dropTarget(), 72 | isOver: monitor.isOver(), 73 | canDrop: monitor.canDrop() 74 | }); 75 | 76 | // Export the wrapped component: 77 | export default DropTarget(ItemTypes.RESIZABLE_HANDLE, target, collect)(ResizeContainer); 78 | --------------------------------------------------------------------------------