├── .gitignore ├── README.md ├── app ├── Log.jsx ├── TextField.jsx └── app.jsx ├── dist ├── bundle.js └── positionable.js ├── index.html ├── jsx ├── PlacementController.jsx ├── Positionable.jsx ├── RotationController.jsx ├── Scale-mixin.js ├── ScaleController.jsx ├── Transform-mixin.js └── ZIndexController.jsx ├── manifest.json ├── package.json └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-positionable-component 2 | 3 | ## This is a WIP test repository at the moment. 4 | 5 | 6 | ## Installation 7 | 8 | Install for use in your own project with `npm install react-positionable-component`. 9 | 10 | You'll probably want to use the `./style.css` file as positionable stylesheet, 11 | or, alternatively, roll your own by looking at which rules are used for what. 12 | 13 | ## Development 14 | 15 | For dev work on this code, clone, `npm install` and then to run, `npm run dev`. 16 | 17 | Individual `npm run` tasks: 18 | 19 | - build the code with `npm run build:pos` or `npm run build:app` 20 | - watch the code with `npm run watch:pos` or `npm run watch:app` 21 | - run a live-server with the `live-server` cli utility 22 | 23 | ## License 24 | 25 | MPL 2.0 26 | 27 | ## Demo 28 | 29 | [http://pomax.github.io/react-positionable-component](http://pomax.github.io/react-positionable-component) 30 | -------------------------------------------------------------------------------- /app/Log.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | var Log = React.createClass({ 4 | 5 | getInitialState: function() { 6 | return { 7 | messages: [] 8 | }; 9 | }, 10 | 11 | componentWillMount: function() { 12 | document.addEventListener("app:log", this.logMessage); 13 | }, 14 | 15 | logMessage: function(evt) { 16 | var msg = evt.detail.msg; 17 | this.setState({ 18 | messages: this.state.messages.concat([msg]) 19 | }, function() { 20 | this.getDOMNode().scrollTo(0,99999999999999999); 21 | }); 22 | }, 23 | 24 | render: function() { 25 | var messages = this.state.messages.map(function(m) { 26 | return
{m}
; 27 | }); 28 | return
{messages}
; 29 | }, 30 | 31 | componentWillUnmount: function() { 32 | document.removeEventListener("app:log", this.logMessage); 33 | }, 34 | 35 | }); 36 | 37 | module.exports = Log; 38 | -------------------------------------------------------------------------------- /app/TextField.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react"); 4 | 5 | var TextField = React.createClass({ 6 | getInitialState: function() { 7 | return { value: "" }; 8 | }, 9 | render: function() { 10 | return 11 | }, 12 | update: function(evt) { 13 | this.setState({ 14 | value: evt.target.value 15 | }); 16 | } 17 | }) 18 | 19 | module.exports = TextField; 20 | -------------------------------------------------------------------------------- /app/app.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react"); 4 | 5 | // Main components: 6 | var Positionable = require('../jsx/Positionable.jsx'); 7 | var TextField = require('./TextField.jsx'); 8 | var Log = require('./Log.jsx'); 9 | 10 | // Main page content for testing: 11 | var content = ( 12 |
13 | 14 | 15 | 16 |

Draggable, resizable, rotate... rota... rotateable? Whatever: manipulable elements

17 | 18 |

Grab any element that lights up green when you mouseover, and try manipulating it.

19 | 20 |
21 | 22 |
23 | 24 |
25 |

You can do this with a bit of React and CSS3 transforms? What...

26 |
27 | 28 |
29 | all the things 30 |
31 | 32 |
33 | ); 34 | 35 | // and finally, load all that stuff. 36 | var target = document.getElementById('app'); 37 | React.render(content, target); 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Positionables 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /jsx/PlacementController.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react"); 4 | 5 | var PlacementController = React.createClass({ 6 | mixins: [ 7 | require("./Transform-mixin"), 8 | require("./Scale-mixin") 9 | ], 10 | 11 | render: function() { 12 | return
13 |
14 | 15 | 16 |
17 |
; 18 | }, 19 | 20 | handleTransform: function() { 21 | if (this.props.onChange) { 22 | var x = this.state.x + this.state.xDiff; 23 | var y = this.state.y + this.state.yDiff; 24 | this.props.onChange(x, y); 25 | } 26 | } 27 | }); 28 | 29 | module.exports = PlacementController; 30 | 65254 -------------------------------------------------------------------------------- /jsx/Positionable.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react"); 4 | var classes = require("classnames"); 5 | 6 | var PlacementController = require("./PlacementController.jsx"); 7 | var RotationController = require("./RotationController.jsx"); 8 | var ScaleController = require("./ScaleController.jsx"); 9 | var ZIndexController = require("./ZIndexController.jsx"); 10 | 11 | var onClickOutside = require("react-onclickoutside"); 12 | 13 | var Positionable = React.createClass({ 14 | 15 | mixins: [ onClickOutside ], 16 | 17 | handleClickOutside: function() { 18 | this.stopHandling(); 19 | this.setState({ 20 | activated: false 21 | }); 22 | }, 23 | 24 | getInitialState: function() { 25 | return { 26 | x: this.props.x || 0, 27 | y: this.props.y || 0, 28 | angle: this.props.angle || 0, 29 | scale: this.props.scale || 1, 30 | zIndex: this.props.zIndex || 1, 31 | activated: !!this.props.activated || false 32 | }; 33 | }, 34 | 35 | render: function() { 36 | var x = this.state.x, 37 | y = this.state.y, 38 | angle = (180 * this.state.angle / Math.PI), 39 | scale = this.state.scale, 40 | zIndex = this.state.zIndex; 41 | 42 | var style = { 43 | transform: [ 44 | "translate("+x+"px, "+y+"px)", 45 | "rotate("+angle+"deg)", 46 | "scale("+scale+")" 47 | ].join(" "), 48 | transformOrigin: "center", 49 | zIndex: zIndex 50 | }; 51 | 52 | var className = classes({ 53 | positionable: true, 54 | activated: this.state.activated 55 | }); 56 | 57 | var controls = [ 58 | 64 | , 65 | 70 | , 71 | 76 | , 77 | 80 | ]; 81 | 82 | return ( 83 |
87 | { this.state.activated ? controls : false } 88 | { this.props.children } 89 |
90 | ); 91 | }, 92 | 93 | toggle: function(evt) { 94 | this.setState({ 95 | activated: !this.state.activated 96 | }); 97 | }, 98 | 99 | startHandlingTouch: function() { 100 | this.setState({ 101 | activated: true 102 | }); 103 | }, 104 | 105 | startHandling: function() { 106 | this.handling = true; 107 | }, 108 | 109 | stopHandling: function() { 110 | if(this.handling && this.props.clickHandler && !this.manipulating) { 111 | this.props.clickHandler(this); 112 | } 113 | this.manipulating = false; 114 | this.handling = false; 115 | }, 116 | 117 | handleTranslation: function(x, y) { 118 | this.manipulating = true; 119 | this.setState({ 120 | x: x, 121 | y: y 122 | }); 123 | }, 124 | 125 | handleRotation: function(angle) { 126 | this.manipulating = true; 127 | this.setState({ 128 | angle: angle 129 | }); 130 | }, 131 | 132 | handleScaling: function(scale) { 133 | this.manipulating = true; 134 | this.setState({ 135 | scale: scale 136 | }, function() { 137 | // make sure all the controls are counter-scale if scale < 1 138 | var counterScale = 1/scale; 139 | ["rotation","scale","placement","zIndex"].forEach(function(c) { 140 | this.refs[c+"Controller"].setScale(counterScale); 141 | }.bind(this)); 142 | }); 143 | }, 144 | 145 | handleZIndexChange: function(zIndex) { 146 | this.manipulating = true; 147 | this.setState({ 148 | zIndex: zIndex 149 | }); 150 | }, 151 | 152 | getTransform: function() { 153 | return { 154 | x: this.state.x, 155 | y: this.state.y, 156 | angle: this.state.angle, 157 | scale: this.state.scale, 158 | zIndex: this.state.zIndex 159 | }; 160 | }, 161 | 162 | setTransform: function(obj) { 163 | this.setState({ 164 | x: obj.x || 0, 165 | y: obj.y || 0, 166 | angle: obj.angle || 0, 167 | scale: obj.scale || 1, 168 | zIndex: obj.zIndex || 0 169 | }); 170 | } 171 | }); 172 | 173 | module.exports = Positionable; 174 | -------------------------------------------------------------------------------- /jsx/RotationController.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react"); 4 | 5 | var RotationController = React.createClass({ 6 | mixins: [ 7 | require("./Transform-mixin"), 8 | require("./Scale-mixin") 9 | ], 10 | 11 | getInitialState: function() { 12 | return { 13 | base: this.props.angle || 0, 14 | angle: 0 15 | }; 16 | }, 17 | 18 | render: function() { 19 | 20 | return
21 |
22 |
; 23 | }, 24 | 25 | handleTransform: function() { 26 | var s = this.state, p = this.props; 27 | 28 | if (p.origin && p.onChange) { 29 | var node = p.origin.getDOMNode(); 30 | var dims = node.getBoundingClientRect(); 31 | var xOffset = dims.left + (dims.right - dims.left)/2; 32 | var yOffset = dims.top + (dims.bottom - dims.top)/2; 33 | 34 | // normalised vector 1: 35 | var x1 = s.xMark - xOffset, 36 | y1 = s.yMark - yOffset, 37 | m1 = Math.sqrt(x1*x1 + y1*y1); 38 | x1 /= m1; 39 | y1 /= m1; 40 | 41 | // normalised vector 2: 42 | var x2 = (s.xMark + s.xDiff) - xOffset, 43 | y2 = (s.yMark + s.yDiff) - yOffset, 44 | m2 = Math.sqrt(x2*x2 + y2*y2); 45 | x2 /= m2; 46 | y2 /= m2; 47 | 48 | // signed angle between these vectors: 49 | var cross = x1*y2 - y1*x2; 50 | var dot = x1*x2 + y1*y2; 51 | var angle = Math.atan2(cross, dot); 52 | 53 | // communicate angle to owner 54 | this.setState( 55 | { angle: angle }, 56 | function() { p.onChange(this.state.base + angle); } 57 | ); 58 | } 59 | }, 60 | 61 | handleTransformEnd: function() { 62 | this.setState({ 63 | base: this.state.base + this.state.angle 64 | }); 65 | } 66 | }); 67 | 68 | module.exports = RotationController; 69 | -------------------------------------------------------------------------------- /jsx/Scale-mixin.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getInitialState: function() { 3 | return { 4 | stylingScale: 1 5 | }; 6 | }, 7 | 8 | getScaleStyle: function() { 9 | return { 10 | transform: "scale(" + (this.state.stylingScale || 1) + ")", 11 | transformOrigin: "center center" 12 | } 13 | }, 14 | 15 | setScale: function(stylingScale) { 16 | this.setState({ 17 | stylingScale: stylingScale 18 | }); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /jsx/ScaleController.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react"); 4 | 5 | var ScaleController = React.createClass({ 6 | mixins: [ 7 | require("./Transform-mixin"), 8 | require("./Scale-mixin") 9 | ], 10 | 11 | getInitialState: function() { 12 | return { base: this.props.scale || 1, scale: 1 }; 13 | }, 14 | 15 | render: function() { 16 | return
17 |
18 |
; 19 | }, 20 | 21 | handleTransform: function() { 22 | var s = this.state, p = this.props; 23 | if (p.origin && p.onChange) { 24 | var node = p.origin.getDOMNode(); 25 | var dims = node.getBoundingClientRect(); 26 | var xOffset = dims.left + (dims.right - dims.left)/2; 27 | var yOffset = dims.top + (dims.bottom - dims.top)/2; 28 | 29 | // vector 1: 30 | var x1 = s.xMark - xOffset, 31 | y1 = s.yMark - yOffset; 32 | 33 | // vector 2: 34 | var x2 = (s.xMark + s.xDiff) - xOffset, 35 | y2 = (s.yMark + s.yDiff) - yOffset; 36 | 37 | // normalised vector 1: 38 | var m1 = Math.sqrt(x1*x1 + y1*y1), 39 | nx1 = x1 / m1, 40 | ny1 = y1 / m1; 41 | 42 | // projection of vector 2 onto vector 1 involves 43 | // finding the projection scale factor, which is 44 | // exactly what we need: 45 | var scale = (x2*nx1 + y2*ny1)/m1; 46 | 47 | // communicate scale to owner 48 | this.setState( 49 | { scale: scale }, 50 | function() { p.onChange(this.state.base * scale); } 51 | ); 52 | } 53 | }, 54 | 55 | handleTransformEnd: function() { 56 | this.setState({ 57 | base: this.state.base * this.state.scale, 58 | scale: 1 59 | }); 60 | } 61 | }); 62 | 63 | module.exports = ScaleController; 64 | 65 | -------------------------------------------------------------------------------- /jsx/Transform-mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function fixTouchEvent(evt) { 4 | evt.clientX = parseInt( evt.touches[0].pageX, 10); 5 | evt.clientY = parseInt( evt.touches[0].pageY, 10); 6 | } 7 | 8 | 9 | module.exports = { 10 | 11 | mixins: [ 12 | require("react-onclickoutside") 13 | ], 14 | 15 | handleClickOutside: function(evt) { 16 | if(this.state.repositioning) { 17 | this.endReposition(); 18 | this.endRepositionTouch(); 19 | } 20 | }, 21 | 22 | getInitialState: function() { 23 | var stayactive = false; 24 | if (typeof this.props.stayactive !== "undefined") { 25 | stayactive = !!this.props.stayactive; 26 | } 27 | 28 | return { 29 | activated: false, 30 | active: false, 31 | stayactive: stayactive, 32 | x: this.props.x || 0, 33 | y: this.props.y || 0, 34 | xMark: 0, 35 | yMark: 0, 36 | xDiff: 0, 37 | yDiff: 0 38 | }; 39 | }, 40 | 41 | componentWillMount: function() { 42 | var activated = false; 43 | if (typeof this.props.activated !== "undefined") { 44 | activated = !!this.props.activated; 45 | } 46 | if(activated) this.setState({ activated: activated}); 47 | }, 48 | 49 | componentDidMount: function() { 50 | var thisNode = this.getDOMNode(); 51 | thisNode.addEventListener("mousedown", this.startReposition); 52 | thisNode.addEventListener("touchstart", this.startRepositionTouch); 53 | }, 54 | 55 | componentWillUnmount: function() { 56 | this.stopListening(); 57 | var thisNode = this.getDOMNode(); 58 | thisNode.removeEventListener("mousedown", this.startReposition); 59 | thisNode.removeEventListener("touchstart", this.startRepositionTouch); 60 | }, 61 | 62 | toggle: function(evt) { 63 | this.setState({ 64 | activated: !this.state.activated 65 | }); 66 | }, 67 | 68 | 69 | 70 | /**************************************************************** 71 | * MOUSE EVENT HANDLING 72 | ****************************************************************/ 73 | 74 | startReposition: function(evt) { 75 | if (this.state.activated) { 76 | evt.stopPropagation(); 77 | this.setState({ 78 | active: true, 79 | xMark: evt.clientX, 80 | yMark: evt.clientY, 81 | xDiff: 0, 82 | yDiff: 0 83 | }); 84 | this.listenForRepositioning(); 85 | } 86 | }, 87 | 88 | listenForRepositioning: function() { 89 | document.addEventListener("mousemove", this.reposition); 90 | document.addEventListener("mouseup", this.endReposition); 91 | }, 92 | 93 | reposition: function(evt) { 94 | if(this.state.active) { 95 | evt.stopPropagation(); 96 | evt.preventDefault(); 97 | this.setState({ 98 | xDiff: evt.clientX - this.state.xMark, 99 | yDiff: evt.clientY - this.state.yMark 100 | }, function() { 101 | if (this.handleTransform) { 102 | this.handleTransform(); 103 | } 104 | }); 105 | } 106 | }, 107 | 108 | endReposition: function(evt) { 109 | if(this.state.active) { 110 | evt.stopPropagation(); 111 | this.stopListening(); 112 | this.setState({ 113 | active: false, 114 | x: this.state.x + this.state.xDiff, 115 | y: this.state.y + this.state.yDiff, 116 | xDiff: 0, 117 | yDiff: 0 118 | }); 119 | if (this.handleTransformEnd) { 120 | this.handleTransformEnd(); 121 | } 122 | } 123 | }, 124 | 125 | stopListening: function() { 126 | document.removeEventListener("mousemove", this.reposition); 127 | document.removeEventListener("mouseup", this.endReposition); 128 | }, 129 | 130 | /**************************************************************** 131 | * TOUCH EVENT HANDLING 132 | ****************************************************************/ 133 | 134 | startRepositionTouch: function(evt) { 135 | if (this.state.activated) { 136 | evt.stopPropagation(); 137 | evt.preventDefault(); 138 | fixTouchEvent(evt); 139 | this.setState({ 140 | active: true, 141 | xMark: evt.clientX, 142 | yMark: evt.clientY, 143 | xDiff: 0, 144 | yDiff: 0 145 | }); 146 | this.listenForRepositioningTouch(); 147 | } 148 | }, 149 | 150 | listenForRepositioningTouch: function() { 151 | document.addEventListener("touchmove", this.repositionTouch); 152 | document.addEventListener("touchend", this.endRepositionTouch); 153 | }, 154 | 155 | repositionTouch: function(evt) { 156 | if(this.state.active) { 157 | evt.stopPropagation(); 158 | evt.preventDefault(); 159 | fixTouchEvent(evt); 160 | this.setState({ 161 | xDiff: evt.clientX - this.state.xMark, 162 | yDiff: evt.clientY - this.state.yMark 163 | }, function() { 164 | if (this.handleTransform) { 165 | this.handleTransform(); 166 | } 167 | }); 168 | } 169 | }, 170 | 171 | endRepositionTouch: function() { 172 | if(this.state.active) { 173 | this.stopListeningTouch(); 174 | this.setState({ 175 | active: false, 176 | x: this.state.x + this.state.xDiff, 177 | y: this.state.y + this.state.yDiff, 178 | xDiff: 0, 179 | yDiff: 0 180 | }, function() { 181 | if (this.handleTransformEnd) { 182 | this.handleTransformEnd(); 183 | } 184 | }); 185 | } 186 | }, 187 | 188 | stopListeningTouch: function() { 189 | document.removeEventListener("touchmove", this.repositionTouch); 190 | document.removeEventListener("touchend", this.endRepositionTouch); 191 | } 192 | }; 193 | -------------------------------------------------------------------------------- /jsx/ZIndexController.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react"); 4 | 5 | var ZIndexController = React.createClass({ 6 | 7 | mixins: [ 8 | require("./Scale-mixin") 9 | ], 10 | 11 | getInitialState: function() { 12 | return { 13 | zIndex: this.props.zIndex || 0 14 | }; 15 | }, 16 | 17 | render: function() { 18 | return ( 19 |
20 |
21 | layer position: {this.state.zIndex} 22 |
23 |
24 | ); 25 | }, 26 | 27 | zUp: function(evt) { 28 | evt.stopPropagation(); 29 | this.setState({ zIndex: this.state.zIndex + 1 }, function() { 30 | if(this.props.onChange) { 31 | this.props.onChange(this.state.zIndex); 32 | } 33 | }); 34 | }, 35 | 36 | zDown: function(evt) { 37 | evt.stopPropagation(); 38 | this.setState({ zIndex: Math.max(0, this.state.zIndex - 1) }, function() { 39 | if(this.props.onChange) { 40 | this.props.onChange(this.state.zIndex); 41 | } 42 | }); 43 | } 44 | }); 45 | 46 | module.exports = ZIndexController; 47 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Web Application Manifest Sample", 3 | "start_url": "index.html", 4 | "display": "standalone", 5 | "orientation": "portrait" 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-positionable-component", 3 | "version": "1.0.10", 4 | "description": "A wrapper component to make arbitrary content easier to CSS transform", 5 | "author": "Pomax", 6 | "main": "jsx/Positionable.jsx", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Pomax/react-positionable-component" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "positionable", 14 | "CSS", 15 | "transform" 16 | ], 17 | "license": "MPL 2.0", 18 | "bugs": { 19 | "url": "https://github.com/Pomax/react-positionable-component/issues" 20 | }, 21 | "homepage": "https://github.com/Pomax/react-positionable-component", 22 | "scripts": { 23 | "build:pos": "browserify -t reactify ./jsx/Positionable.jsx | uglifyjs --screw-ie8 > ./dist/positionable.js", 24 | "build:app": "browserify -t reactify ./app/app.jsx -o ./dist/bundle.js", 25 | "watch:pos": "watch \"npm run build:pos\" ./jsx", 26 | "watch:app": "watch \"npm run build:app\" ./app", 27 | "dev": "parallelshell \"npm run watch:pos\" \"npm run watch:app\" \"live-server\"" 28 | }, 29 | "dependencies": { 30 | "classnames": "^1.2.0", 31 | "react": "^0.13.1", 32 | "react-hammerjs": "^0.2.1", 33 | "react-onclickoutside": "^0.2.3", 34 | "uglifyjs": "^2.4.10" 35 | }, 36 | "devDependencies": { 37 | "browserify": "^9.0.7", 38 | "parallelshell": "^1.1.1", 39 | "reactify": "^1.1.0", 40 | "watch": "^0.15.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-user-select: none; 3 | -moz-user-select: none; 4 | -ms-user-select: none; 5 | -o-user-select: none; 6 | user-select: none; 7 | } 8 | 9 | htlm, body { 10 | margin: 0; 11 | padding: 1em; 12 | } 13 | 14 | .positionable { 15 | position: relative; 16 | display: inline-block; 17 | z-index: 0; 18 | background: inherit; 19 | padding: 1em; 20 | } 21 | 22 | .positionable { 23 | background: rgba(0,255,0,0.2); 24 | border: 1px dashed rgba(0,0,0,0.4); 25 | margin: -1px; 26 | } 27 | 28 | .placement-control { display: none; } 29 | .positionable .placement-control { 30 | position: absolute; 31 | display: block; 32 | top: -1.1em; 33 | left: -1.0em; 34 | cursor: pointer; 35 | } 36 | .positionable .placement-control div { 37 | width: 1em; 38 | height: 1em; 39 | padding: 1em; 40 | background: rgba(0,0,0,0.3); 41 | border-radius: 3em; 42 | } 43 | .positionable .placement-control span { 44 | position: relative; 45 | display: inline-block; 46 | } 47 | .positionable .placement-control span + span { 48 | vertical-align: 0.05em; 49 | margin-left: -0.75em; 50 | } 51 | 52 | .rotation-control { display: none; } 53 | .positionable .rotation-control { 54 | position: absolute; 55 | display: block; 56 | top: 1.6em; 57 | right: -1.6em; 58 | cursor: pointer; 59 | transform-origin: 0.5em 0.5em; 60 | transform: rotate(-135deg); 61 | } 62 | .positionable .rotation-control div { 63 | width: 1em; 64 | height: 1em; 65 | padding: 1em; 66 | background: rgba(0,0,0,0.3); 67 | border-radius: 3em; 68 | } 69 | 70 | .scale-control { display: none; } 71 | .positionable .scale-control { 72 | position: absolute; 73 | display: block; 74 | bottom: -0.9em; 75 | right: -1.3em; 76 | cursor: pointer; 77 | } 78 | .positionable .scale-control div { 79 | width: 1em; 80 | height: 1em; 81 | padding: 1em; 82 | background: rgba(0,0,0,0.3); 83 | border-radius: 3em; 84 | } 85 | 86 | .zindex-controller { display: none; } 87 | .positionable .zindex-controller { 88 | position: absolute; 89 | display: block; 90 | bottom: 0; 91 | left: 1em; 92 | opacity: 0.7; 93 | font-size: 90%; 94 | white-space: nowrap; 95 | } 96 | 97 | .positionable .zindex-controller .zmod { 98 | position: relative; 99 | cursor: pointer; 100 | opacity: 0.5; 101 | padding: 1em; 102 | margin: -1.1em; 103 | border-radius: 2em; 104 | border: none; 105 | background: transparent; 106 | } 107 | .positionable .zindex-controller .zmod.left { 108 | padding-right: 0.25em; 109 | margin-right: -0.25em; 110 | } 111 | .positionable .zindex-controller .zmod.right { 112 | padding-left: 0.25em; 113 | margin-left: -0.25em; 114 | } 115 | 116 | .log { 117 | float: right; 118 | height: 80vh; 119 | width: 20em; 120 | overflow: auto; 121 | } --------------------------------------------------------------------------------