react-crossword-v2
Version:
A flexible, responsive, and easy-to-use crossword component for React apps
167 lines • 9.05 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const prop_types_1 = __importDefault(require("prop-types"));
const styled_components_1 = __importStar(require("styled-components"));
const Cell_1 = __importDefault(require("./Cell"));
const context_1 = require("./context");
// import {
// } from './types';
const defaultTheme = {
columnBreakpoint: '768px',
gridBackground: 'rgb(0,0,0)',
cellBackground: 'rgb(255,255,255)',
cellBorder: 'rgb(0,0,0)',
textColor: 'rgb(0,0,0)',
numberColor: 'rgba(0,0,0, 0.25)',
focusBackground: 'rgb(255,255,0)',
highlightBackground: 'rgb(255,255,204)',
};
const GridWrapper = styled_components_1.default.div.attrs(( /* props */) => ({
className: 'crossword grid',
})) `
/* position: relative; */
/* min-width: 20rem; */
/* max-width: 60rem; Should the size matter? */
width: auto;
flex: 2 1 50%;
`;
const CrosswordGridPropTypes = {
/** presentation values for the crossword; these override any values coming from a parent ThemeProvider context. */
theme: prop_types_1.default.shape({
/** browser-width at which the clues go from showing beneath the grid to showing beside the grid */
columnBreakpoint: prop_types_1.default.string,
/** overall background color (fill) for the crossword grid; can be `'transparent'` to show through a page background image */
gridBackground: prop_types_1.default.string,
/** background for an answer cell */
cellBackground: prop_types_1.default.string,
/** border for an answer cell */
cellBorder: prop_types_1.default.string,
/** color for answer text (entered by the player) */
textColor: prop_types_1.default.string,
/** color for the across/down numbers in the grid */
numberColor: prop_types_1.default.string,
/** background color for the cell with focus, the one that the player is typing into */
focusBackground: prop_types_1.default.string,
/** background color for the cells in the answer the player is working on,
* helps indicate in which direction focus will be moving; also used as a
* background on the active clue */
highlightBackground: prop_types_1.default.string,
}),
};
// export interface CrosswordGridImperative {
// /**
// * Sets focus to the crossword component.
// */
// focus: () => void;
// }
/**
* The rendering component for the crossword grid itself.
*/
function CrosswordGrid({ theme }) {
const { size, gridData, handleInputKeyDown, handleInputChange, handleCellClick, handleInputClick, registerFocusHandler, focused, selectedPosition: { row: focusedRow, col: focusedCol }, selectedDirection: currentDirection, selectedNumber: currentNumber, } = (0, react_1.useContext)(context_1.CrosswordContext);
const inputRef = (0, react_1.useRef)(null);
const contextTheme = (0, react_1.useContext)(styled_components_1.ThemeContext);
// focus and movement
const focus = (0, react_1.useCallback)(() => {
var _a;
// console.log('CrosswordGrid.focus()', { haveRef: !!inputRef.current });
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
}, []);
(0, react_1.useEffect)(() => {
// focus.name = 'CrosswordGrid.focus()';
registerFocusHandler(focus);
return () => {
registerFocusHandler(null);
};
}, [focus, registerFocusHandler]);
// // expose some imperative methods -- should we do this here, or make the
// // caller go through the crossword provider?
// useImperativeHandle(
// ref,
// () => ({
// /**
// * Sets focus to the crossword component.
// */
// focus: () => { console.log('CrosswordGrid.focus() imperative!'); focus();},
// }),
// [focus]
// );
// We have several properties that we bundle together as context for the
// cells, rather than have them as independent properties. (Or should they
// stay separate? Or be passed as "spread" values?)
const cellSize = 100 / size;
const cellPadding = 0.125;
const cellInner = cellSize - cellPadding * 2;
const cellHalf = cellSize / 2;
const fontSize = cellInner * 0.7;
const sizeContext = (0, react_1.useMemo)(() => ({ cellSize, cellPadding, cellInner, cellHalf, fontSize }), [cellSize, cellPadding, cellInner, cellHalf, fontSize]);
const inputStyle = (0, react_1.useMemo)(() => ({
position: 'absolute',
// In order to ensure the top/left positioning makes sense,
// there is an absolutely-positioned <div> with no
// margin/padding that we *don't* expose to consumers. This
// keeps the math much more reliable. (But we're still
// seeing a slight vertical deviation towards the bottom of
// the grid! The "* 0.995" seems to help.)
top: `calc(${focusedRow * cellSize * 0.995}% + 2px)`,
left: `calc(${focusedCol * cellSize}% + 2px)`,
width: `calc(${cellSize}% - 4px)`,
height: `calc(${cellSize}% - 4px)`,
fontSize: `${fontSize * 6}px`,
textAlign: 'center',
textAnchor: 'middle',
backgroundColor: 'transparent',
caretColor: 'transparent',
margin: 0,
padding: 0,
border: 0,
cursor: 'default',
pointerEvents: 'none',
}), [cellSize, focusedRow, focusedCol, fontSize]);
// The final theme is the merger of three values: the "theme" property
// passed to the component (which takes precedence), any values from
// ThemeContext, and finally the "defaultTheme" values fill in for any
// needed ones that are missing. (We create this in standard last-one-wins
// order in Javascript, of course.)
const finalTheme = (0, react_1.useMemo)(() => (Object.assign(Object.assign(Object.assign({}, defaultTheme), contextTheme), theme)), [defaultTheme, contextTheme, theme]);
return ((0, jsx_runtime_1.jsx)(context_1.CrosswordSizeContext.Provider, Object.assign({ value: sizeContext }, { children: (0, jsx_runtime_1.jsx)(styled_components_1.ThemeProvider, Object.assign({ theme: finalTheme }, { children: (0, jsx_runtime_1.jsx)(GridWrapper, { children: (0, jsx_runtime_1.jsxs)("div", Object.assign({ style: { margin: 0, padding: 0, position: 'relative' } }, { children: [(0, jsx_runtime_1.jsxs)("svg", Object.assign({ viewBox: "0 0 100 100" }, { children: [(0, jsx_runtime_1.jsx)("rect", { x: 0, y: 0, width: 100, height: 100, fill: finalTheme.gridBackground }, void 0), gridData.flatMap((rowData, row) => rowData.map((cellData, col) => cellData.used ? (
// Should the Cell figure out its focus/highlight state
// directly from the CrosswordContext?
(0, jsx_runtime_1.jsx)(Cell_1.default
// eslint-disable-next-line react/no-array-index-key
, { cellData: cellData, focus: focused && row === focusedRow && col === focusedCol, highlight: focused &&
!!currentNumber &&
cellData[currentDirection] === currentNumber, onClick: handleCellClick }, `R${row}C${col}`)) : undefined))] }), void 0), (0, jsx_runtime_1.jsx)("input", { ref: inputRef, "aria-label": "crossword-input", type: "text", onClick: handleInputClick, onKeyDown: handleInputKeyDown, onChange: handleInputChange, value: "",
// onInput={this.handleInput}
autoComplete: "off", spellCheck: "false", autoCorrect: "off", readOnly: true, style: inputStyle }, void 0)] }), void 0) }, void 0) }), void 0) }), void 0));
}
exports.default = CrosswordGrid;
CrosswordGrid.propTypes = CrosswordGridPropTypes;
CrosswordGrid.defaultProps = {
theme: null,
};
//# sourceMappingURL=CrosswordGrid.js.map