UNPKG

react-crossword-v2

Version:

A flexible, responsive, and easy-to-use crossword component for React apps

167 lines 9.05 kB
"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