├── .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 |
23 |
24 |
25 | You can do this with a bit of React and CSS3 transforms? What...
26 |
27 |
28 |
29 |
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 ;
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 ;
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 | }
--------------------------------------------------------------------------------