From fdce577b595a6261dc2bea3bdf778b970c06e717 Mon Sep 17 00:00:00 2001 From: Demetrios Skamiotis Date: Fri, 8 Mar 2024 10:45:39 +0000 Subject: [PATCH 1/5] move add slot in crossword ad for option 2 --- .../projects/common/modules/crosswords/crossword.js | 3 ++- static/src/stylesheets/_vars.scss | 5 +++++ static/src/stylesheets/module/_adslot.scss | 7 ++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/static/src/javascripts/projects/common/modules/crosswords/crossword.js b/static/src/javascripts/projects/common/modules/crosswords/crossword.js index b83591377a9a..c90812783063 100644 --- a/static/src/javascripts/projects/common/modules/crosswords/crossword.js +++ b/static/src/javascripts/projects/common/modules/crosswords/crossword.js @@ -802,12 +802,13 @@ class Crossword extends Component { {anagramHelper} +
-
+ Date: Tue, 19 Mar 2024 13:21:57 +0000 Subject: [PATCH 2/5] Rename crosswords advert css --- static/src/stylesheets/_vars.scss | 4 ++-- static/src/stylesheets/module/_adslot.scss | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/src/stylesheets/_vars.scss b/static/src/stylesheets/_vars.scss index f3b9ef772e18..be4f7761938f 100644 --- a/static/src/stylesheets/_vars.scss +++ b/static/src/stylesheets/_vars.scss @@ -168,8 +168,8 @@ $garnett-x-large-button-icon: 24px; // Mobile Sticky // ============================================================================= -$mobile-sticky-width: 320px; -$mobile-sticky-height: 74px; +$crosswords-advert-width: 320px; +$crosswords-advert-height: 74px; // MPU // ============================================================================= diff --git a/static/src/stylesheets/module/_adslot.scss b/static/src/stylesheets/module/_adslot.scss index 803f81039663..4de831ac969e 100644 --- a/static/src/stylesheets/module/_adslot.scss +++ b/static/src/stylesheets/module/_adslot.scss @@ -105,14 +105,14 @@ #dfp-ad--crossword-banner-mobile { padding: 10px 0 14px; - width: $mobile-sticky-width; - height: $mobile-sticky-height; + width: $crosswords-advert-width; + height: $crosswords-advert-height; background-color: #ffffff; } .ad-slot-container--centre-slot.crossword-mobile-banner-ad { - display: flex; + @include mq($from: phablet) { display: none; } From c426162af2f598869f02a5be99422b454370cc69 Mon Sep 17 00:00:00 2001 From: Dominik Lander Date: Tue, 19 Mar 2024 13:25:50 +0000 Subject: [PATCH 3/5] prettier --- .../common/modules/crosswords/crossword.js | 1597 +++++++++-------- 1 file changed, 801 insertions(+), 796 deletions(-) diff --git a/static/src/javascripts/projects/common/modules/crosswords/crossword.js b/static/src/javascripts/projects/common/modules/crosswords/crossword.js index c90812783063..181708aa65bd 100644 --- a/static/src/javascripts/projects/common/modules/crosswords/crossword.js +++ b/static/src/javascripts/projects/common/modules/crosswords/crossword.js @@ -12,811 +12,816 @@ import { Controls } from 'common/modules/crosswords/controls'; import { HiddenInput } from 'common/modules/crosswords/hidden-input'; import { Grid } from 'common/modules/crosswords/grid'; import { - buildClueMap, - buildGrid, - otherDirection, - entryHasCell, - cluesFor, - mapGrid, - getClearableCellsForClue, - getLastCellInClue, - getPreviousClueInGroup, - isFirstCellInClue, - getNextClueInGroup, - isLastCellInClue, - gridSize, - checkClueHasBeenAnswered, - buildSeparatorMap, - cellsForEntry, + buildClueMap, + buildGrid, + otherDirection, + entryHasCell, + cluesFor, + mapGrid, + getClearableCellsForClue, + getLastCellInClue, + getPreviousClueInGroup, + isFirstCellInClue, + getNextClueInGroup, + isLastCellInClue, + gridSize, + checkClueHasBeenAnswered, + buildSeparatorMap, + cellsForEntry, } from 'common/modules/crosswords/helpers'; import { keycodes } from 'common/modules/crosswords/keycodes'; import { - saveGridState, - loadGridState, + saveGridState, + loadGridState, } from 'common/modules/crosswords/persistence'; import { classNames } from 'common/modules/crosswords/classNames'; - class Crossword extends Component { - constructor(props) { - super(props); - const dimensions = this.props.data.dimensions; - - this.columns = dimensions.cols; - this.rows = dimensions.rows; - this.clueMap = buildClueMap(this.props.data.entries); - - this.state = { - grid: buildGrid( - dimensions.rows, - dimensions.cols, - this.props.data.entries, - loadGridState(this.props.data.id) - ), - cellInFocus: null, - directionOfEntry: null, - showAnagramHelper: false, - }; - } - - componentDidMount() { - // Sticky clue - const $stickyClueWrapper = $(findDOMNode(this.stickyClueWrapper)); - const $grid = $(findDOMNode(this.grid)); - const $game = $(findDOMNode(this.game)); - - mediator.on( - 'window:resize', - debounce(this.setGridHeight.bind(this), 200) - ); - mediator.on( - 'window:orientationchange', - debounce(this.setGridHeight.bind(this), 200) - ); - this.setGridHeight(); - - mediator.on('window:throttledScroll', () => { - const gridOffset = $grid.offset(); - const gameOffset = $game.offset(); - const stickyClueWrapperOffset = $stickyClueWrapper.offset(); - const scrollY = window.scrollY; - - fastdom.mutate(() => { - // Clear previous state - $stickyClueWrapper - .css('top', '') - .css('bottom', '') - .removeClass('is-fixed'); - - const scrollYPastGame = scrollY - gameOffset.top; - - if (scrollYPastGame >= 0) { - const gridOffsetBottom = gridOffset.top + gridOffset.height; - - if ( - scrollY > - gridOffsetBottom - stickyClueWrapperOffset.height - ) { - $stickyClueWrapper.css('top', 'auto').css('bottom', 0); - } else if (isIOS()) { - // iOS doesn't support sticky things when the keyboard - // is open, so we use absolute positioning and - // programatically update the value of top - $stickyClueWrapper.css('top', scrollYPastGame); - } else { - $stickyClueWrapper.addClass('is-fixed'); - } - } - }); - }); - } - - componentDidUpdate(prevProps, prevState) { - // return focus to active cell after exiting anagram helper - if ( - !this.state.showAnagramHelper && - this.state.showAnagramHelper !== prevState.showAnagramHelper - ) { - this.focusCurrentCell(); - } - } - - onKeyDown(event) { - const cell = this.state.cellInFocus; - - if (event.keyCode === keycodes.tab) { - event.preventDefault(); - if (event.shiftKey) { - this.focusPreviousClue(); - } else { - this.focusNextClue(); - } - } else if (!event.metaKey && !event.ctrlKey && !event.altKey) { - if ( - event.keyCode === keycodes.backspace || - event.keyCode === keycodes.delete - ) { - event.preventDefault(); - if (cell) { - if (this.cellIsEmpty(cell.x, cell.y)) { - this.focusPrevious(); - } else { - this.setCellValue(cell.x, cell.y, ''); - this.save(); - } - } - } else if (event.keyCode === keycodes.left) { - event.preventDefault(); - this.moveFocus(-1, 0); - } else if (event.keyCode === keycodes.up) { - event.preventDefault(); - this.moveFocus(0, -1); - } else if (event.keyCode === keycodes.right) { - event.preventDefault(); - this.moveFocus(1, 0); - } else if (event.keyCode === keycodes.down) { - event.preventDefault(); - this.moveFocus(0, 1); - } - } - } - - // called when cell is selected (by click or programtically focussed) - onSelect(x, y) { - const cellInFocus = this.state.cellInFocus; - const clue = cluesFor(this.clueMap, x, y); - const focussedClue = this.clueInFocus(); - let newDirection; - - const isInsideFocussedClue = () => - focussedClue ? entryHasCell(focussedClue, x, y) : false; - - if ( - cellInFocus && - cellInFocus.x === x && - cellInFocus.y === y && - this.state.directionOfEntry - ) { - /** User has clicked again on the highlighted cell, meaning we ought to swap direction */ - newDirection = otherDirection(this.state.directionOfEntry); - - if (clue[newDirection]) { - this.focusClue(x, y, newDirection); - } - } else if (isInsideFocussedClue() && this.state.directionOfEntry) { - /** - * If we've clicked inside the currently highlighted clue, then we ought to just shift the cursor - * to the new cell, not change direction or anything funny. - */ - - this.focusClue(x, y, this.state.directionOfEntry); - } else { - this.state.cellInFocus = { - x, - y, - }; - - const isStartOfClue = (sourceClue) => - !!sourceClue && - sourceClue.position.x === x && - sourceClue.position.y === y; - - /** - * If the user clicks on the start of a down clue midway through an across clue, we should - * prefer to highlight the down clue. - */ - if (!isStartOfClue(clue.across) && isStartOfClue(clue.down)) { - newDirection = 'down'; - } else if (clue.across) { - /** Across is the default focus otherwise */ - newDirection = 'across'; - } else { - newDirection = 'down'; - } - this.focusClue(x, y, newDirection); - } - } - - onCheat() { - this.allHighlightedClues().forEach(clue => this.cheat(clue)); - this.save(); - } - - onCheck() { - // 'Check this' checks single and grouped clues - this.allHighlightedClues().forEach(clue => this.check(clue)); - this.save(); - } - - onSolution() { - this.props.data.entries.forEach(clue => this.cheat(clue)); - this.save(); - } - - onCheckAll() { - this.props.data.entries.forEach(clue => this.check(clue)); - this.save(); - } - - onClearAll() { - this.setState({ - grid: mapGrid(this.state.grid, cell => { - cell.value = ''; - return cell; - }), - }); - - this.save(); - } - - onClearSingle() { - const clueInFocus = this.clueInFocus(); - - if (clueInFocus) { - // Merge arrays of cells from all highlighted clues - // const cellsInFocus = _.flatten(_.map(this.allHighlightedClues(), helpers.cellsForEntry, this)); - const cellsInFocus = getClearableCellsForClue( - this.state.grid, - this.clueMap, - this.props.data.entries, - clueInFocus - ); - - this.setState({ - grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { - if ( - cellsInFocus.some(c => c.x === gridX && c.y === gridY) - ) { - cell.value = ''; - } - return cell; - }), - }); - - this.save(); - } - } - - onToggleAnagramHelper() { - // only show anagram helper if a clue is active - if (!this.state.showAnagramHelper) { - if (this.clueInFocus()) { - this.setState({ - showAnagramHelper: true, - }); - } - } else { - this.setState({ - showAnagramHelper: false, - }); - } - } - - onClickHiddenInput(event) { - const focussed = this.state.cellInFocus; - - if (focussed) { - this.onSelect(focussed.x, focussed.y); - } - - /* We need to handle touch seperately as touching an input on iPhone does not fire the + constructor(props) { + super(props); + const dimensions = this.props.data.dimensions; + + this.columns = dimensions.cols; + this.rows = dimensions.rows; + this.clueMap = buildClueMap(this.props.data.entries); + + this.state = { + grid: buildGrid( + dimensions.rows, + dimensions.cols, + this.props.data.entries, + loadGridState(this.props.data.id), + ), + cellInFocus: null, + directionOfEntry: null, + showAnagramHelper: false, + }; + } + + componentDidMount() { + // Sticky clue + const $stickyClueWrapper = $(findDOMNode(this.stickyClueWrapper)); + const $grid = $(findDOMNode(this.grid)); + const $game = $(findDOMNode(this.game)); + + mediator.on( + 'window:resize', + debounce(this.setGridHeight.bind(this), 200), + ); + mediator.on( + 'window:orientationchange', + debounce(this.setGridHeight.bind(this), 200), + ); + this.setGridHeight(); + + mediator.on('window:throttledScroll', () => { + const gridOffset = $grid.offset(); + const gameOffset = $game.offset(); + const stickyClueWrapperOffset = $stickyClueWrapper.offset(); + const scrollY = window.scrollY; + + fastdom.mutate(() => { + // Clear previous state + $stickyClueWrapper + .css('top', '') + .css('bottom', '') + .removeClass('is-fixed'); + + const scrollYPastGame = scrollY - gameOffset.top; + + if (scrollYPastGame >= 0) { + const gridOffsetBottom = gridOffset.top + gridOffset.height; + + if ( + scrollY > + gridOffsetBottom - stickyClueWrapperOffset.height + ) { + $stickyClueWrapper.css('top', 'auto').css('bottom', 0); + } else if (isIOS()) { + // iOS doesn't support sticky things when the keyboard + // is open, so we use absolute positioning and + // programatically update the value of top + $stickyClueWrapper.css('top', scrollYPastGame); + } else { + $stickyClueWrapper.addClass('is-fixed'); + } + } + }); + }); + } + + componentDidUpdate(prevProps, prevState) { + // return focus to active cell after exiting anagram helper + if ( + !this.state.showAnagramHelper && + this.state.showAnagramHelper !== prevState.showAnagramHelper + ) { + this.focusCurrentCell(); + } + } + + onKeyDown(event) { + const cell = this.state.cellInFocus; + + if (event.keyCode === keycodes.tab) { + event.preventDefault(); + if (event.shiftKey) { + this.focusPreviousClue(); + } else { + this.focusNextClue(); + } + } else if (!event.metaKey && !event.ctrlKey && !event.altKey) { + if ( + event.keyCode === keycodes.backspace || + event.keyCode === keycodes.delete + ) { + event.preventDefault(); + if (cell) { + if (this.cellIsEmpty(cell.x, cell.y)) { + this.focusPrevious(); + } else { + this.setCellValue(cell.x, cell.y, ''); + this.save(); + } + } + } else if (event.keyCode === keycodes.left) { + event.preventDefault(); + this.moveFocus(-1, 0); + } else if (event.keyCode === keycodes.up) { + event.preventDefault(); + this.moveFocus(0, -1); + } else if (event.keyCode === keycodes.right) { + event.preventDefault(); + this.moveFocus(1, 0); + } else if (event.keyCode === keycodes.down) { + event.preventDefault(); + this.moveFocus(0, 1); + } + } + } + + // called when cell is selected (by click or programtically focussed) + onSelect(x, y) { + const cellInFocus = this.state.cellInFocus; + const clue = cluesFor(this.clueMap, x, y); + const focussedClue = this.clueInFocus(); + let newDirection; + + const isInsideFocussedClue = () => + focussedClue ? entryHasCell(focussedClue, x, y) : false; + + if ( + cellInFocus && + cellInFocus.x === x && + cellInFocus.y === y && + this.state.directionOfEntry + ) { + /** User has clicked again on the highlighted cell, meaning we ought to swap direction */ + newDirection = otherDirection(this.state.directionOfEntry); + + if (clue[newDirection]) { + this.focusClue(x, y, newDirection); + } + } else if (isInsideFocussedClue() && this.state.directionOfEntry) { + /** + * If we've clicked inside the currently highlighted clue, then we ought to just shift the cursor + * to the new cell, not change direction or anything funny. + */ + + this.focusClue(x, y, this.state.directionOfEntry); + } else { + this.state.cellInFocus = { + x, + y, + }; + + const isStartOfClue = (sourceClue) => + !!sourceClue && + sourceClue.position.x === x && + sourceClue.position.y === y; + + /** + * If the user clicks on the start of a down clue midway through an across clue, we should + * prefer to highlight the down clue. + */ + if (!isStartOfClue(clue.across) && isStartOfClue(clue.down)) { + newDirection = 'down'; + } else if (clue.across) { + /** Across is the default focus otherwise */ + newDirection = 'across'; + } else { + newDirection = 'down'; + } + this.focusClue(x, y, newDirection); + } + } + + onCheat() { + this.allHighlightedClues().forEach((clue) => this.cheat(clue)); + this.save(); + } + + onCheck() { + // 'Check this' checks single and grouped clues + this.allHighlightedClues().forEach((clue) => this.check(clue)); + this.save(); + } + + onSolution() { + this.props.data.entries.forEach((clue) => this.cheat(clue)); + this.save(); + } + + onCheckAll() { + this.props.data.entries.forEach((clue) => this.check(clue)); + this.save(); + } + + onClearAll() { + this.setState({ + grid: mapGrid(this.state.grid, (cell) => { + cell.value = ''; + return cell; + }), + }); + + this.save(); + } + + onClearSingle() { + const clueInFocus = this.clueInFocus(); + + if (clueInFocus) { + // Merge arrays of cells from all highlighted clues + // const cellsInFocus = _.flatten(_.map(this.allHighlightedClues(), helpers.cellsForEntry, this)); + const cellsInFocus = getClearableCellsForClue( + this.state.grid, + this.clueMap, + this.props.data.entries, + clueInFocus, + ); + + this.setState({ + grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { + if ( + cellsInFocus.some((c) => c.x === gridX && c.y === gridY) + ) { + cell.value = ''; + } + return cell; + }), + }); + + this.save(); + } + } + + onToggleAnagramHelper() { + // only show anagram helper if a clue is active + if (!this.state.showAnagramHelper) { + if (this.clueInFocus()) { + this.setState({ + showAnagramHelper: true, + }); + } + } else { + this.setState({ + showAnagramHelper: false, + }); + } + } + + onClickHiddenInput(event) { + const focussed = this.state.cellInFocus; + + if (focussed) { + this.onSelect(focussed.x, focussed.y); + } + + /* We need to handle touch seperately as touching an input on iPhone does not fire the click event - listen for a touchStart and preventDefault to avoid calling onSelect twice on devices that fire click AND touch events. The click event doesn't fire only when the input is already focused */ - if (event.type === 'touchstart') { - event.preventDefault(); - } - } - - setGridHeight() { - if (!this.$gridWrapper) { - this.$gridWrapper = $(findDOMNode(this.gridWrapper)); - } - - if ( - isBreakpoint({ - max: 'tablet', - }) - ) { - fastdom.measure(() => { - // Our grid is a square, set the height of the grid wrapper - // to the width of the grid wrapper - fastdom.mutate(() => { - this.$gridWrapper.css( - 'height', - `${this.$gridWrapper.offset().width}px` - ); - }); - this.gridHeightIsSet = true; - }); - } else if (this.gridHeightIsSet) { - // Remove inline style if tablet and wider - this.$gridWrapper.attr('style', ''); - } - } - - setCellValue(x, y, value) { - this.setState({ - grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { - if (gridX === x && gridY === y) { - cell.value = value; - cell.isError = false; - } - - return cell; - }), - }); - } - - getCellValue(x, y) { - return this.state.grid[x][y].value; - } - - setReturnPosition(position) { - this.returnPosition = position; - } - - - - insertCharacter(character) { - const characterUppercase = character.toUpperCase(); - const cell = this.state.cellInFocus; - if ( - /[A-Za-zÀ-ÿ0-9]/.test(characterUppercase) && - characterUppercase.length === 1 && - cell - ) { - this.setCellValue(cell.x, cell.y, characterUppercase); - this.save(); - this.focusNext(); - } - } - - cellIsEmpty(x, y) { - return !this.getCellValue(x, y); - } - - goToReturnPosition() { - if ( - isBreakpoint({ - max: 'mobile', - }) - ) { - if (this.returnPosition) { - scrollTo(this.returnPosition, 250, 'easeOutQuad'); - } - this.returnPosition = null; - } - } - - indexOfClueInFocus() { - return this.props.data.entries.indexOf(this.clueInFocus()); - } - - focusPreviousClue() { - const i = this.indexOfClueInFocus(); - const entries = this.props.data.entries; - - if (i !== -1) { - const newClue = entries[i === 0 ? entries.length - 1 : i - 1]; - this.focusClue( - newClue.position.x, - newClue.position.y, - newClue.direction - ); - } - } - - focusNextClue() { - const i = this.indexOfClueInFocus(); - const entries = this.props.data.entries; - - if (i !== -1) { - const newClue = entries[i === entries.length - 1 ? 0 : i + 1]; - this.focusClue( - newClue.position.x, - newClue.position.y, - newClue.direction - ); - } - } - - moveFocus(deltaX, deltaY) { - const cell = this.state.cellInFocus; - - if (!cell) { - return; - } - - const x = cell.x + deltaX; - const y = cell.y + deltaY; - let direction = 'down'; - - if ( - this.state.grid[x] && - this.state.grid[x][y] && - this.state.grid[x][y].isEditable - ) { - if (deltaY !== 0) { - direction = 'down'; - } else if (deltaX !== 0) { - direction = 'across'; - } - this.focusClue(x, y, direction); - } - } - - isAcross() { - return this.state.directionOfEntry === 'across'; - } - - focusPrevious() { - const cell = this.state.cellInFocus; - const clue = this.clueInFocus(); - - if (cell && clue) { - if (isFirstCellInClue(cell, clue)) { - const newClue = getPreviousClueInGroup( - this.props.data.entries, - clue - ); - if (newClue) { - const newCell = getLastCellInClue(newClue); - this.focusClue(newCell.x, newCell.y, newClue.direction); - } - } else if (this.isAcross()) { - this.moveFocus(-1, 0); - } else { - this.moveFocus(0, -1); - } - } - } - - focusNext() { - const cell = this.state.cellInFocus; - const clue = this.clueInFocus(); - - if (cell && clue) { - if (isLastCellInClue(cell, clue)) { - const newClue = getNextClueInGroup( - this.props.data.entries, - clue - ); - if (newClue) { - this.focusClue( - newClue.position.x, - newClue.position.y, - newClue.direction - ); - } - } else if (this.isAcross()) { - this.moveFocus(1, 0); - } else { - this.moveFocus(0, 1); - } - } - } - - asPercentage(x, y) { - const width = gridSize(this.columns); - const height = gridSize(this.rows); - - return { - x: (100 * x) / width, - y: (100 * y) / height, - }; - } - - focusHiddenInput(x, y) { - const wrapper = (findDOMNode( - this.hiddenInputComponent.wrapper - )); - const left = gridSize(x); - const top = gridSize(y); - const position = this.asPercentage(left, top); - - /** This has to be done before focus to move viewport accordingly */ - wrapper.style.left = `${position.x}%`; - wrapper.style.top = `${position.y}%`; - - const hiddenInputNode = (findDOMNode( - this.hiddenInputComponent.input - )); - - if (document.activeElement !== hiddenInputNode) { - hiddenInputNode.focus(); - } - } - - // Focus corresponding clue for a given cell - focusClue(x, y, direction) { - const clues = cluesFor(this.clueMap, x, y); - const clue = clues[direction]; - - if (clues && clue) { - this.focusHiddenInput(x, y); - - this.setState({ - grid: this.state.grid, - cellInFocus: { - x, - y, - }, - directionOfEntry: direction, - }); - - // Side effect - window.history.replaceState( - undefined, - document.title, - `#${clue.id}` - ); - } - } - - // Focus first cell in given clue - focusFirstCellInClue(entry) { - this.focusClue(entry.position.x, entry.position.y, entry.direction); - } - - focusCurrentCell() { - if (this.state.cellInFocus) { - this.focusHiddenInput( - this.state.cellInFocus.x, - this.state.cellInFocus.y - ); - } - } - - clueInFocus() { - if (this.state.cellInFocus) { - const cluesForCell = cluesFor( - this.clueMap, - this.state.cellInFocus.x, - this.state.cellInFocus.y - ); - - if (this.state.directionOfEntry) { - return cluesForCell[this.state.directionOfEntry]; - } - } - return null; - } - - allHighlightedClues() { - return this.props.data.entries.filter(clue => - this.clueIsInFocusGroup(clue) - ); - } - - clueIsInFocusGroup(clue) { - if (this.state.cellInFocus) { - const cluesForCell = cluesFor( - this.clueMap, - this.state.cellInFocus.x, - this.state.cellInFocus.y - ); - - if ( - this.state.directionOfEntry && - cluesForCell[this.state.directionOfEntry] - ) { - return cluesForCell[this.state.directionOfEntry].group.includes( - clue.id - ); - } - } - return false; - } - - cluesData() { - return this.props.data.entries.map(entry => { - const hasAnswered = checkClueHasBeenAnswered( - this.state.grid, - entry - ); - return { - entry, - hasAnswered, - isSelected: this.clueIsInFocusGroup(entry), - }; - }); - } - - save() { - saveGridState(this.props.data.id, this.state.grid); - } - - cheat(entry) { - const cells = cellsForEntry(entry); - - if (entry.solution) { - this.setState({ - grid: mapGrid(this.state.grid, (cell, x, y) => { - if (cells.some(c => c.x === x && c.y === y)) { - const n = - entry.direction === 'across' - ? x - entry.position.x - : y - entry.position.y; - - cell.value = entry.solution[n]; - } - - return cell; - }), - }); - } - } - - check(entry) { - const cells = cellsForEntry(entry); - - if (entry.solution) { - const badCells = zip(cells, entry.solution.split('')) - .filter(cellAndSolution => { - const coords = cellAndSolution[0]; - const cell = this.state.grid[coords.x][coords.y]; - const solution = cellAndSolution[1]; - return ( - /^[A-Z]$/.test(cell.value) && cell.value !== solution - ); - }) - .map(cellAndSolution => cellAndSolution[0]); - - this.setState({ - grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { - if ( - badCells.some(bad => bad.x === gridX && bad.y === gridY) - ) { - cell.isError = true; - cell.value = ''; - } - - return cell; - }), - }); - - setTimeout(() => { - this.setState({ - grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { - if ( - badCells.some( - bad => bad.x === gridX && bad.y === gridY - ) - ) { - cell.isError = false; - cell.value = ''; - } - - return cell; - }), - }); - }, 150); - } - } - - hiddenInputValue() { - const cell = this.state.cellInFocus; - - let currentValue; - - if (cell) { - currentValue = this.state.grid[cell.x][cell.y].value; - } - - return currentValue || ''; - } - - hasSolutions() { - return 'solution' in this.props.data.entries[0]; - } - - isHighlighted(x, y) { - const focused = this.clueInFocus(); - return focused - ? focused.group.some(id => { - const entry = this.props.data.entries.find(e => e.id === id); - return entryHasCell(entry, x, y); - }) - : false; - } - - render() { - const focused = this.clueInFocus(); - - const anagramHelper = this.state.showAnagramHelper && ( - - ); - - const gridProps = { - rows: this.rows, - columns: this.columns, - cells: this.state.grid, - separators: buildSeparatorMap(this.props.data.entries), - crossword: this, - focussedCell: this.state.cellInFocus, - ref: grid => { - this.grid = grid; - }, - }; - // Trigger the custom event when component has loaded for ad slot in commercial - useEffect(() => { - const customEvent = new CustomEvent('crossword-loaded'); - window.dispatchEvent(customEvent); - }); - - return ( -
-
{ - this.game = game; - }}> -
{ - this.stickyClueWrapper = stickyClueWrapper; - }}> -
- {focused && ( -
-
- - {focused.number}{' '} - - {focused.direction}{' '} - - - -
-
- )} -
-
-
{ - this.gridWrapper = gridWrapper; - }}> - {Grid(gridProps)} - { - this.hiddenInputComponent = hiddenInputComponent; - }} - /> - {anagramHelper} -
-
-
- - - -
- ); - } + if (event.type === 'touchstart') { + event.preventDefault(); + } + } + + setGridHeight() { + if (!this.$gridWrapper) { + this.$gridWrapper = $(findDOMNode(this.gridWrapper)); + } + + if ( + isBreakpoint({ + max: 'tablet', + }) + ) { + fastdom.measure(() => { + // Our grid is a square, set the height of the grid wrapper + // to the width of the grid wrapper + fastdom.mutate(() => { + this.$gridWrapper.css( + 'height', + `${this.$gridWrapper.offset().width}px`, + ); + }); + this.gridHeightIsSet = true; + }); + } else if (this.gridHeightIsSet) { + // Remove inline style if tablet and wider + this.$gridWrapper.attr('style', ''); + } + } + + setCellValue(x, y, value) { + this.setState({ + grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { + if (gridX === x && gridY === y) { + cell.value = value; + cell.isError = false; + } + + return cell; + }), + }); + } + + getCellValue(x, y) { + return this.state.grid[x][y].value; + } + + setReturnPosition(position) { + this.returnPosition = position; + } + + insertCharacter(character) { + const characterUppercase = character.toUpperCase(); + const cell = this.state.cellInFocus; + if ( + /[A-Za-zÀ-ÿ0-9]/.test(characterUppercase) && + characterUppercase.length === 1 && + cell + ) { + this.setCellValue(cell.x, cell.y, characterUppercase); + this.save(); + this.focusNext(); + } + } + + cellIsEmpty(x, y) { + return !this.getCellValue(x, y); + } + + goToReturnPosition() { + if ( + isBreakpoint({ + max: 'mobile', + }) + ) { + if (this.returnPosition) { + scrollTo(this.returnPosition, 250, 'easeOutQuad'); + } + this.returnPosition = null; + } + } + + indexOfClueInFocus() { + return this.props.data.entries.indexOf(this.clueInFocus()); + } + + focusPreviousClue() { + const i = this.indexOfClueInFocus(); + const entries = this.props.data.entries; + + if (i !== -1) { + const newClue = entries[i === 0 ? entries.length - 1 : i - 1]; + this.focusClue( + newClue.position.x, + newClue.position.y, + newClue.direction, + ); + } + } + + focusNextClue() { + const i = this.indexOfClueInFocus(); + const entries = this.props.data.entries; + + if (i !== -1) { + const newClue = entries[i === entries.length - 1 ? 0 : i + 1]; + this.focusClue( + newClue.position.x, + newClue.position.y, + newClue.direction, + ); + } + } + + moveFocus(deltaX, deltaY) { + const cell = this.state.cellInFocus; + + if (!cell) { + return; + } + + const x = cell.x + deltaX; + const y = cell.y + deltaY; + let direction = 'down'; + + if ( + this.state.grid[x] && + this.state.grid[x][y] && + this.state.grid[x][y].isEditable + ) { + if (deltaY !== 0) { + direction = 'down'; + } else if (deltaX !== 0) { + direction = 'across'; + } + this.focusClue(x, y, direction); + } + } + + isAcross() { + return this.state.directionOfEntry === 'across'; + } + + focusPrevious() { + const cell = this.state.cellInFocus; + const clue = this.clueInFocus(); + + if (cell && clue) { + if (isFirstCellInClue(cell, clue)) { + const newClue = getPreviousClueInGroup( + this.props.data.entries, + clue, + ); + if (newClue) { + const newCell = getLastCellInClue(newClue); + this.focusClue(newCell.x, newCell.y, newClue.direction); + } + } else if (this.isAcross()) { + this.moveFocus(-1, 0); + } else { + this.moveFocus(0, -1); + } + } + } + + focusNext() { + const cell = this.state.cellInFocus; + const clue = this.clueInFocus(); + + if (cell && clue) { + if (isLastCellInClue(cell, clue)) { + const newClue = getNextClueInGroup( + this.props.data.entries, + clue, + ); + if (newClue) { + this.focusClue( + newClue.position.x, + newClue.position.y, + newClue.direction, + ); + } + } else if (this.isAcross()) { + this.moveFocus(1, 0); + } else { + this.moveFocus(0, 1); + } + } + } + + asPercentage(x, y) { + const width = gridSize(this.columns); + const height = gridSize(this.rows); + + return { + x: (100 * x) / width, + y: (100 * y) / height, + }; + } + + focusHiddenInput(x, y) { + const wrapper = findDOMNode(this.hiddenInputComponent.wrapper); + const left = gridSize(x); + const top = gridSize(y); + const position = this.asPercentage(left, top); + + /** This has to be done before focus to move viewport accordingly */ + wrapper.style.left = `${position.x}%`; + wrapper.style.top = `${position.y}%`; + + const hiddenInputNode = findDOMNode(this.hiddenInputComponent.input); + + if (document.activeElement !== hiddenInputNode) { + hiddenInputNode.focus(); + } + } + + // Focus corresponding clue for a given cell + focusClue(x, y, direction) { + const clues = cluesFor(this.clueMap, x, y); + const clue = clues[direction]; + + if (clues && clue) { + this.focusHiddenInput(x, y); + + this.setState({ + grid: this.state.grid, + cellInFocus: { + x, + y, + }, + directionOfEntry: direction, + }); + + // Side effect + window.history.replaceState( + undefined, + document.title, + `#${clue.id}`, + ); + } + } + + // Focus first cell in given clue + focusFirstCellInClue(entry) { + this.focusClue(entry.position.x, entry.position.y, entry.direction); + } + + focusCurrentCell() { + if (this.state.cellInFocus) { + this.focusHiddenInput( + this.state.cellInFocus.x, + this.state.cellInFocus.y, + ); + } + } + + clueInFocus() { + if (this.state.cellInFocus) { + const cluesForCell = cluesFor( + this.clueMap, + this.state.cellInFocus.x, + this.state.cellInFocus.y, + ); + + if (this.state.directionOfEntry) { + return cluesForCell[this.state.directionOfEntry]; + } + } + return null; + } + + allHighlightedClues() { + return this.props.data.entries.filter((clue) => + this.clueIsInFocusGroup(clue), + ); + } + + clueIsInFocusGroup(clue) { + if (this.state.cellInFocus) { + const cluesForCell = cluesFor( + this.clueMap, + this.state.cellInFocus.x, + this.state.cellInFocus.y, + ); + + if ( + this.state.directionOfEntry && + cluesForCell[this.state.directionOfEntry] + ) { + return cluesForCell[this.state.directionOfEntry].group.includes( + clue.id, + ); + } + } + return false; + } + + cluesData() { + return this.props.data.entries.map((entry) => { + const hasAnswered = checkClueHasBeenAnswered( + this.state.grid, + entry, + ); + return { + entry, + hasAnswered, + isSelected: this.clueIsInFocusGroup(entry), + }; + }); + } + + save() { + saveGridState(this.props.data.id, this.state.grid); + } + + cheat(entry) { + const cells = cellsForEntry(entry); + + if (entry.solution) { + this.setState({ + grid: mapGrid(this.state.grid, (cell, x, y) => { + if (cells.some((c) => c.x === x && c.y === y)) { + const n = + entry.direction === 'across' + ? x - entry.position.x + : y - entry.position.y; + + cell.value = entry.solution[n]; + } + + return cell; + }), + }); + } + } + + check(entry) { + const cells = cellsForEntry(entry); + + if (entry.solution) { + const badCells = zip(cells, entry.solution.split('')) + .filter((cellAndSolution) => { + const coords = cellAndSolution[0]; + const cell = this.state.grid[coords.x][coords.y]; + const solution = cellAndSolution[1]; + return ( + /^[A-Z]$/.test(cell.value) && cell.value !== solution + ); + }) + .map((cellAndSolution) => cellAndSolution[0]); + + this.setState({ + grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { + if ( + badCells.some( + (bad) => bad.x === gridX && bad.y === gridY, + ) + ) { + cell.isError = true; + cell.value = ''; + } + + return cell; + }), + }); + + setTimeout(() => { + this.setState({ + grid: mapGrid(this.state.grid, (cell, gridX, gridY) => { + if ( + badCells.some( + (bad) => bad.x === gridX && bad.y === gridY, + ) + ) { + cell.isError = false; + cell.value = ''; + } + + return cell; + }), + }); + }, 150); + } + } + + hiddenInputValue() { + const cell = this.state.cellInFocus; + + let currentValue; + + if (cell) { + currentValue = this.state.grid[cell.x][cell.y].value; + } + + return currentValue || ''; + } + + hasSolutions() { + return 'solution' in this.props.data.entries[0]; + } + + isHighlighted(x, y) { + const focused = this.clueInFocus(); + return focused + ? focused.group.some((id) => { + const entry = this.props.data.entries.find( + (e) => e.id === id, + ); + return entryHasCell(entry, x, y); + }) + : false; + } + + render() { + const focused = this.clueInFocus(); + + const anagramHelper = this.state.showAnagramHelper && ( + + ); + + const gridProps = { + rows: this.rows, + columns: this.columns, + cells: this.state.grid, + separators: buildSeparatorMap(this.props.data.entries), + crossword: this, + focussedCell: this.state.cellInFocus, + ref: (grid) => { + this.grid = grid; + }, + }; + // Trigger the custom event when component has loaded for ad slot in commercial + useEffect(() => { + const customEvent = new CustomEvent('crossword-loaded'); + window.dispatchEvent(customEvent); + }); + + return ( +
+
{ + this.game = game; + }} + > +
{ + this.stickyClueWrapper = stickyClueWrapper; + }} + > +
+ {focused && ( +
+
+ + {focused.number}{' '} + + {focused.direction}{' '} + + + +
+
+ )} +
+
+
{ + this.gridWrapper = gridWrapper; + }} + > + {Grid(gridProps)} + { + this.hiddenInputComponent = + hiddenInputComponent; + }} + /> + {anagramHelper} +
+
+
+ +
+ +
+ ); + } } export default Crossword; From 33b298d1b54d6733b7f38d40e689ae8287b2154d Mon Sep 17 00:00:00 2001 From: Dominik Lander Date: Wed, 20 Mar 2024 10:47:29 +0000 Subject: [PATCH 4/5] move spacing to ad container --- static/src/stylesheets/module/_adslot.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/src/stylesheets/module/_adslot.scss b/static/src/stylesheets/module/_adslot.scss index 4de831ac969e..edf03b1f48e0 100644 --- a/static/src/stylesheets/module/_adslot.scss +++ b/static/src/stylesheets/module/_adslot.scss @@ -103,8 +103,8 @@ margin: 0 auto; } -#dfp-ad--crossword-banner-mobile { - padding: 10px 0 14px; + +.ad-slot-container .ad-slot--crossword-banner-mobile { width: $crosswords-advert-width; height: $crosswords-advert-height; background-color: #ffffff; @@ -112,6 +112,7 @@ .ad-slot-container--centre-slot.crossword-mobile-banner-ad { display: flex; + margin: 8px auto; @include mq($from: phablet) { display: none; From fa7b3a8df99db1fa8c27bd102b9962fbff073aed Mon Sep 17 00:00:00 2001 From: Dominik Lander Date: Wed, 20 Mar 2024 14:45:25 +0000 Subject: [PATCH 5/5] bump commercial. update comment --- package.json | 2 +- static/src/stylesheets/_vars.scss | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 02fcd0a264d8..9db36e7c7e80 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@guardian/ab-core": "^5.0.0", "@guardian/atom-renderer": "^2.0.0", "@guardian/automat-modules": "^0.3.8", - "@guardian/commercial": "17.4.0", + "@guardian/commercial": "17.4.1", "@guardian/consent-management-platform": "13.12.0", "@guardian/core-web-vitals": "^5.0.0", "@guardian/identity-auth": "2.1.0", diff --git a/static/src/stylesheets/_vars.scss b/static/src/stylesheets/_vars.scss index be4f7761938f..e7a074288dfd 100644 --- a/static/src/stylesheets/_vars.scss +++ b/static/src/stylesheets/_vars.scss @@ -166,7 +166,7 @@ $garnett-large-button-icon: 20px; $garnett-x-large-button-icon: 24px; -// Mobile Sticky +// Crosswords mobile banner advert // ============================================================================= $crosswords-advert-width: 320px; $crosswords-advert-height: 74px; diff --git a/yarn.lock b/yarn.lock index fea2372b1dbf..332385463a3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2423,10 +2423,10 @@ react "^16.13.1" react-dom "^16.13.1" -"@guardian/commercial@17.4.0": - version "17.4.0" - resolved "https://registry.yarnpkg.com/@guardian/commercial/-/commercial-17.4.0.tgz#c7c33cfc90a7b7414513e53a70bd2af49344ea40" - integrity sha512-UQqjLoGfJCvUlXyLbFNhLZNftxjVjm0j0nru/VoLrM7voykSF7rwk0tOQPrBZvLMVS+W8MXKg5DA2F3BCEvYJA== +"@guardian/commercial@17.4.1": + version "17.4.1" + resolved "https://registry.yarnpkg.com/@guardian/commercial/-/commercial-17.4.1.tgz#6befb2b743e90a04eeed4956ead6a5a4d22f6384" + integrity sha512-slXPGzGVJHk/ENgREUV7aGKZ0OVXkeQv5ivwwu1HIQujT5LYs+ykkKfmOQbay2uAOJGgMKA3CYqKiqmr4GjFLA== dependencies: "@changesets/cli" "^2.26.2" "@guardian/ophan-tracker-js" "2.0.4"