WIP
parent
6f4cd8bff5
commit
e43c5312fc
@ -1,6 +1,6 @@
|
|||||||
/* NOTE: The 'glo-bal' filename of this file is a workaround to deal with `insert-module-globals` incorrectly detecting this file to contain a reference to a `g lobal` variable (due to the icssify-generated class names including the filename) */
|
/* NOTE: The 'glo-bal' filename of this file is a workaround to deal with `insert-module-globals` incorrectly detecting this file to contain a reference to a `g lobal` variable (due to the icssify-generated class names including the filename) */
|
||||||
|
|
||||||
.uilibComponent {
|
.uilibThemedElement {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
row-gap: 2px;
|
row-gap: 2px;
|
||||||
column-gap: 2px;
|
column-gap: 2px;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,138 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const useMeasure = require("react-use-measure");
|
||||||
|
const syncpipe = require("syncpipe");
|
||||||
|
const defaultValue = require("default-value");
|
||||||
|
const clamp = require("clamp");
|
||||||
|
|
||||||
|
const useTheme = require("../../util/themeable");
|
||||||
|
const defaultStyle = require("./style.css");
|
||||||
|
const childrenWithProps = require("../../util/children-with-props");
|
||||||
|
const generateGridTemplateString = require("../../util/generate-grid-template-string");
|
||||||
|
const useIndexedState = require("../../util/use-indexed-state");
|
||||||
|
const sum = require("../../util/sum");
|
||||||
|
|
||||||
|
const MINIMUM_PANE_SIZE = 16;
|
||||||
|
|
||||||
|
// FIXME: Logic to constrain resizing when increasing a pane's size further would make the paneset exceed its size boundaries
|
||||||
|
// FIXME: Disable selection globally during dragging
|
||||||
|
|
||||||
|
function selectSmallestSize(sizes) {
|
||||||
|
console.log("sizes", sizes);
|
||||||
|
if (sizes.some((size) => size != null && isNaN(size))) {
|
||||||
|
throw new Error(`NaN encountered!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncpipe(sizes, [
|
||||||
|
(_) => _.filter((size) => size != null),
|
||||||
|
(_) => (_.length > 0)
|
||||||
|
? Math.min(... _)
|
||||||
|
: undefined
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function PaneSet({ vertical, horizontal, children }) {
|
||||||
|
let { withTheme } = useTheme({ control: "paneSet", defaultStyle });
|
||||||
|
let [ boundsRef, bounds ] = useMeasure({ debounce: 10 });
|
||||||
|
let [ newSizes, setNewSize, resetNewSizes ] = useIndexedState();
|
||||||
|
// FIXME: How to deal with new children? Reset all newSizes?
|
||||||
|
|
||||||
|
let direction = (vertical === true)
|
||||||
|
? "vertical"
|
||||||
|
: "horizontal";
|
||||||
|
|
||||||
|
let ownSize = (direction === "vertical")
|
||||||
|
? bounds.height
|
||||||
|
: bounds.width;
|
||||||
|
|
||||||
|
let childrenArray = React.Children.toArray(children);
|
||||||
|
|
||||||
|
let minimumSizes = React.useMemo(() => {
|
||||||
|
return childrenArray.map((child) => {
|
||||||
|
return selectSmallestSize([ child.props.minimumSize, MINIMUM_PANE_SIZE ]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: Detect when this fails
|
||||||
|
let autoPaneMinimumSize = syncpipe(childrenArray, [
|
||||||
|
(_) => _.findIndex((child) => child.resizable !== true),
|
||||||
|
(_) => minimumSizes[_]
|
||||||
|
]);
|
||||||
|
|
||||||
|
let maximumSizes = React.useMemo(() => {
|
||||||
|
let paneSizes = childrenArray.map((child, i) => {
|
||||||
|
// FIXME: Require initialSize when resizable
|
||||||
|
return defaultValue(newSizes[i], child.props.initialSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
return childrenArray.map((_, ownIndex) => {
|
||||||
|
let otherPaneSizes = syncpipe(paneSizes, [
|
||||||
|
(_) => _.filter((size, i) => size != null && i !== ownIndex),
|
||||||
|
(_) => sum(_)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return clamp(ownSize - otherPaneSizes - autoPaneMinimumSize, 0, Infinity);
|
||||||
|
});
|
||||||
|
}, [ childrenArray, newSizes, ownSize ]);
|
||||||
|
|
||||||
|
let autoSizeSeen = 0;
|
||||||
|
|
||||||
|
// NOTE: Not using React.Children.map because it will ignore null return values
|
||||||
|
let handleSides = childrenArray.map((child) => {
|
||||||
|
if (child.props.resizable === true) {
|
||||||
|
if (autoSizeSeen > 0) {
|
||||||
|
return (direction === "vertical")
|
||||||
|
? "n" // north, top
|
||||||
|
: "w"; // west, left
|
||||||
|
} else {
|
||||||
|
return (direction === "vertical")
|
||||||
|
? "s" // south, bottom
|
||||||
|
: "e"; // east, right
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
autoSizeSeen += 1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let gridTemplateProperty = (direction === "vertical")
|
||||||
|
? "gridTemplateRows"
|
||||||
|
: "gridTemplateColumns";
|
||||||
|
|
||||||
|
let gridTemplateString = syncpipe(handleSides, [
|
||||||
|
(_) => _.map((handle) => handle != null ? "auto" : 1),
|
||||||
|
(_) => generateGridTemplateString(_)
|
||||||
|
]);
|
||||||
|
|
||||||
|
let style = {
|
||||||
|
[gridTemplateProperty]: gridTemplateString
|
||||||
|
};
|
||||||
|
|
||||||
|
if (autoSizeSeen !== 1) {
|
||||||
|
// We don't allow *multiple* auto-sized panes, to avoid weird behaviour where the auto-layouted panes may not end up exactly where the resize handle is
|
||||||
|
throw new Error(`Exactly one (center) pane must be non-resizable!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={boundsRef} className={withTheme("uilibComponent", "paneSet", `direction-${direction}`)} style={style}>
|
||||||
|
{childrenWithProps(children, (child, i) => {
|
||||||
|
let handleSide = handleSides[i];
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResized: (newSize) => {
|
||||||
|
setNewSize(i, newSize);
|
||||||
|
},
|
||||||
|
maximumSize: selectSmallestSize([ child.props.maximumSize, maximumSizes[i] ]),
|
||||||
|
// Note that we shouldn't override an explicitly user-configured handleSide
|
||||||
|
... (handleSide == null)
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
handleSide: defaultValue(child.props.handleSide, handleSide),
|
||||||
|
direction: direction
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
:import("../pane/style.css") { pane: pane; }
|
||||||
|
|
||||||
|
.paneSet {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direction-horizontal {
|
||||||
|
.pane {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* FIXME: This should really be & > ..., but that isn't allowed by the parser */
|
||||||
|
:global .react-resizable {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.direction-vertical {
|
||||||
|
.pane {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { ResizableBox } = require("react-resizable");
|
||||||
|
const defaultValue = require("default-value");
|
||||||
|
|
||||||
|
const useDisableSelection = require("../../util/use-disable-selection");
|
||||||
|
const useTheme = require("../../util/themeable");
|
||||||
|
const defaultStyle = require("./style.css");
|
||||||
|
|
||||||
|
function Handle({ ... props }) {
|
||||||
|
let { withTheme } = useTheme({ control: "pane", defaultStyle });
|
||||||
|
|
||||||
|
// NOTE: We need to forward all props to make the mouse events work, which react-resizable internally adds to the component
|
||||||
|
return (
|
||||||
|
<div className={withTheme("handleHitbox")} {... props}>
|
||||||
|
<div className={withTheme("handle")} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Only exact pixel values currently supported for initialSize, add some sort of percentage support in the future?
|
||||||
|
// FIXME: Increase handle hitbox size
|
||||||
|
// FIXME: `Resizable` wrapper element
|
||||||
|
|
||||||
|
// NOTE: onResized and direction are internal, handleSide too but it can be overwritten
|
||||||
|
module.exports = function Pane({ onResized, resizable, initialSize, minimumSize, maximumSize, direction, handleSide, children }) {
|
||||||
|
let { withTheme } = useTheme({ control: "pane", defaultStyle });
|
||||||
|
let { disableGlobalSelection, enableGlobalSelection } = useDisableSelection();
|
||||||
|
|
||||||
|
// FIXME: Validate inputs
|
||||||
|
|
||||||
|
let sizeProps = (direction === "vertical")
|
||||||
|
? {
|
||||||
|
height: initialSize,
|
||||||
|
minConstraints: [ 0, defaultValue(minimumSize, 0) ],
|
||||||
|
maxConstraints: [ Infinity, defaultValue(maximumSize, Infinity) ],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width: initialSize,
|
||||||
|
minConstraints: [ defaultValue(minimumSize, 0), 0 ],
|
||||||
|
maxConstraints: [ defaultValue(maximumSize, Infinity), Infinity ],
|
||||||
|
};
|
||||||
|
|
||||||
|
let axisProp = (direction === "vertical")
|
||||||
|
? "y"
|
||||||
|
: "x";
|
||||||
|
|
||||||
|
function handleOnResizeStop(node, event) {
|
||||||
|
let newSize = (direction === "vertical")
|
||||||
|
? event.size.height
|
||||||
|
: event.size.width;
|
||||||
|
|
||||||
|
onResized(newSize);
|
||||||
|
enableGlobalSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnResizeStart() {
|
||||||
|
disableGlobalSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Needed to prevent react-resizable from somehow breaking hooks inside of Handle
|
||||||
|
function handleFunction() {
|
||||||
|
return <Handle />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Do we still need the contentWrapper, without the bounds-measuring logic?
|
||||||
|
let content = (
|
||||||
|
<div className={withTheme("contentWrapper")}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={withTheme("uilibComponent", "pane", `handleSide-${handleSide}`)}>
|
||||||
|
{(resizable === true)
|
||||||
|
? <ResizableBox handle={handleFunction} resizeHandles={[ handleSide ]} axis={axisProp} onResizeStop={handleOnResizeStop} onResizeStart={handleOnResizeStart} {... sizeProps}>
|
||||||
|
{content}
|
||||||
|
</ResizableBox>
|
||||||
|
: content
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,88 @@
|
|||||||
|
/* FIXME: Move these variables to theme variables eventually */
|
||||||
|
$handleSize: 2px;
|
||||||
|
$hitboxPadding: 5px;
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentWrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleHitbox {
|
||||||
|
position: absolute;
|
||||||
|
/* background-color: red; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleSide-n, .handleSide-s {
|
||||||
|
.handleHitbox {
|
||||||
|
cursor: ns-resize;
|
||||||
|
padding: $hitboxPadding 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
height: $handleSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleSide-e, .handleSide-w {
|
||||||
|
.handleHitbox {
|
||||||
|
cursor: ew-resize;
|
||||||
|
padding: 0 $hitboxPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
width: $handleSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleSide-n {
|
||||||
|
/* north / top */
|
||||||
|
padding-top: $handleSize;
|
||||||
|
|
||||||
|
.handleHitbox {
|
||||||
|
top: calc(0px - $hitboxPadding);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleSide-s {
|
||||||
|
/* south / bottom */
|
||||||
|
padding-bottom: $handleSize;
|
||||||
|
|
||||||
|
.handleHitbox {
|
||||||
|
bottom: calc(0px - $hitboxPadding);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleSide-e {
|
||||||
|
/* east - right */
|
||||||
|
padding-right: $handleSize;
|
||||||
|
|
||||||
|
.handleHitbox {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: calc(0px - $hitboxPadding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleSide-w {
|
||||||
|
/* west - left */
|
||||||
|
padding-left: $handleSize;
|
||||||
|
|
||||||
|
.handleHitbox {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: calc(0px - $hitboxPadding);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
const useTheme = require("../../util/themeable");
|
||||||
|
const defaultStyle = require("./style.css");
|
||||||
|
const generateGridItemStyle = require("../../util/generate-grid-item-style");
|
||||||
|
|
||||||
|
module.exports = function StatusIndicator({ x, y, status, children }) {
|
||||||
|
let { withTheme } = useTheme({ control: "statusIndicator", defaultStyle });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={withTheme("uilibComponent", "statusIndicator", `status-${status}`)} style={generateGridItemStyle({ x, y })}>
|
||||||
|
<div className={withTheme("content")}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
.statusIndicator {
|
||||||
|
composes: centerContent from "../shared.css";
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 4px; /* FIXME: Why do we have this? */
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-positive {
|
||||||
|
background-color: rgb(0, 93, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-negative {
|
||||||
|
background-color: rgb(106, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-neutral {
|
||||||
|
background-color: #5a5a5a;
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
$darkGray: rgb(34, 34, 34);
|
||||||
|
$lessDarkGray: rgb(44, 44, 44);
|
||||||
|
$hoverColor: #e5e5e5;
|
||||||
|
$activeColor: #c5dae5;
|
||||||
|
|
||||||
|
$white: white;
|
||||||
|
$barColor: #edf5f5;
|
||||||
|
$lightGray: silver;
|
||||||
|
|
||||||
|
/* :export {
|
||||||
|
darkGray: $darkGray;
|
||||||
|
hoverColor: $hoverColor;
|
||||||
|
activeColor: $activeColor;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* FIXME: Casemap control names */
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* FIXME: Think about whether this belongs in the theme, or whether it should be scoped to some sort of ApplicationFrame instead. */
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edgeRaise {
|
||||||
|
box-shadow: inset -1px -1px rgba(187, 183, 183, 0.7),
|
||||||
|
inset 1px 1px rgba(206, 206, 206, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edgeLower {
|
||||||
|
box-shadow: inset 1px 1px rgba(187, 183, 183, 0.7),
|
||||||
|
inset -1px -1px rgba(206, 206, 206, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
composes: edgeRaise;
|
||||||
|
background-color: $barColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uilibComponent {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list_list {
|
||||||
|
/* color: red; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.list_item {
|
||||||
|
&:nth-child(odd) {
|
||||||
|
/* background-color: rgb(45, 45, 45); */
|
||||||
|
/* background-color: rgb(51, 51, 51); */
|
||||||
|
background-color: rgb(57, 57, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: rgb(27, 27, 27);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.list_item_selected {
|
||||||
|
/* FIXME: Find a better color for this. */
|
||||||
|
/* background-color: rgb(38, 38, 42); */
|
||||||
|
background-color: blue; /* FIXME: Remove testing color */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_menuBar, .ribbon_ribbon {
|
||||||
|
composes: bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_menuBar > .menu_item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $hoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu_item_selected, &menu_item_directPress {
|
||||||
|
background-color: $activeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_menu {
|
||||||
|
background-color: $barColor;
|
||||||
|
box-shadow: 1px 1px 2px rgb(54, 54, 54);
|
||||||
|
|
||||||
|
& > .menu_item:hover {
|
||||||
|
background-color: $hoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-bottom: 1px solid rgb(78, 78, 78);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_divider {
|
||||||
|
border-bottom: 1px solid rgb(78, 78, 78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_button {
|
||||||
|
background-color: #d0d0d0;
|
||||||
|
/* background-color: magenta; */
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $hoverColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
@include edge-lower;
|
||||||
|
background-color: $activeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.button_selected {
|
||||||
|
background-color: $darkGray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ribbonBox_box {
|
||||||
|
border-left: 1px solid rgba(30, 27, 27, 0.7);
|
||||||
|
border-right: 1px solid rgba(135, 131, 131, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ribbonBox_label {
|
||||||
|
background-color: #f3f1fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar_bar {
|
||||||
|
border: 1px solid rgb(32, 32, 32);
|
||||||
|
/* background-color: rgb(64, 64, 70); */
|
||||||
|
background-color: rgb(43, 43, 47);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar_fill {
|
||||||
|
background-color: rgb(54, 135, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane_handle {
|
||||||
|
background-color: rgb(245, 245, 245);
|
||||||
|
composes: edgeRaise;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ribbon_ribbon {
|
||||||
|
/* ... */
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// NOTE: Only use this where absolutely needed! Normally, the context API should be preferred where possible.
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
module.exports = function childrenWithProps(children, mapper) {
|
||||||
|
return React.Children.map(children, (child, i) => {
|
||||||
|
let extraProps = (typeof mapper === "function")
|
||||||
|
? mapper(child, i)
|
||||||
|
: mapper;
|
||||||
|
|
||||||
|
if (extraProps != null) {
|
||||||
|
// FIXME: Do we need to check React.isValidElement here, to deal with eg. text nodes? Or is that handled internally by React.cloneElement?
|
||||||
|
return React.cloneElement(child, extraProps);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
function normalizeGridCellSize(value) {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return `${value}fr`;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function generateGridTemplateString(values) {
|
||||||
|
return values
|
||||||
|
.map((value) => normalizeGridCellSize(value))
|
||||||
|
.join(" ");
|
||||||
|
};
|
@ -0,0 +1,5 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = function sum(numbers) {
|
||||||
|
return numbers.reduce((total, number) => total + number, 0);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
module.exports = function useDisableSelection() {
|
||||||
|
let body = document.querySelector("body");
|
||||||
|
let lastSelectSetting = React.useRef();
|
||||||
|
|
||||||
|
return {
|
||||||
|
disableGlobalSelection: function () {
|
||||||
|
if (body.style.userSelect !== "none") {
|
||||||
|
lastSelectSetting.current = body.style.userSelect;
|
||||||
|
body.style.userSelect = "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enableGlobalSelection: function () {
|
||||||
|
if (body.style.userSelect === "none" && lastSelectSetting.current != null) {
|
||||||
|
body.style.userSelect = lastSelectSetting.current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const timm = require("timm");
|
||||||
|
|
||||||
|
module.exports = function useIndexedState(initialState = []) {
|
||||||
|
let [ state, setState ] = React.useState(initialState);
|
||||||
|
|
||||||
|
function setIndexedState(index, value) {
|
||||||
|
setState((oldArray) => {
|
||||||
|
let evaluatedValue = (typeof value === "function")
|
||||||
|
? value(oldArray[index])
|
||||||
|
: value;
|
||||||
|
|
||||||
|
return timm.replaceAt(oldArray, index, evaluatedValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
setState((oldArray) => {
|
||||||
|
return oldArray.map(() => undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
state,
|
||||||
|
setIndexedState,
|
||||||
|
resetState
|
||||||
|
];
|
||||||
|
};
|
Loading…
Reference in New Issue