In my form, I want to use keyboard arrow keys to move between the input fields.
Here is the Codesandbox example.
(Click inside the last input field and use the UP arrow key to move up to the previous sibling input field.
Code:
const arrowDownPosition = React.useRef();
const getCurElePosition = (eTarget) => {
const mum = eTarget.parentElement.parentElement;
const parent = mum.parentElement;
const children = Array.from(parent.children);
let taskNum;
children.forEach((el, i) => {
const elChild = el.children[0].children[0];
if (elChild === eTarget) {
taskNum = i;
}
});
return [taskNum, parent, children, mum];
};
const keyUpHandler = (e) => {
e.preventDefault();
if (e.key === "ArrowUp") {
e.target.setSelectionRange(
arrowDownPosition.current[0],
arrowDownPosition.current[1]
);
}
};
const keyHandler = (e) => {
e.stopPropagation();
const eTarget = e.target;
const [elePosition, parent, children, mum] = getCurElePosition(eTarget);
if (e.key === "ArrowUp") {
let previousSiblingRef;
children.forEach((ele, i) => {
if (ele == mum && i - 1 >= 0) {
previousSiblingRef = i - 1;
}
});
if (previousSiblingRef) {
const { selectionStart, selectionEnd } = eTarget;
arrowDownPosition.current = [selectionStart, selectionEnd];
const previousSibling =
children[previousSiblingRef].children[0].children[0];
previousSibling.focus();
previousSibling.setSelectionRange(selectionStart, selectionEnd);
}
}
};
The above solution is a hacky way of solving the problem. You can see the caret move to the start of the previous input field before its moved to my intended position. I am using useRef to store the value.
This is because I am unable to stop the keyUp default handler before my code is run?
Thank you
Related
I would like to shorten this code, but can't figure out how.
The code works in the way that when you press the button in the selector, a map point and a text on the bottom of the map appear. It works in this way it is, but I am sure that there is a way to shorten it. I just have not enough knowledge on how to shorten it.
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.select__item').forEach( function(tabBtn) {
tabBtn.addEventListener('click', function(event) {
const path = event.currentTarget.dataset.path
document.querySelectorAll('.sketch__item',).forEach( function(tabContent) {
tabContent.classList.remove('block-active')
})
document.querySelectorAll('.details__item',).forEach( function(tabContent) {
tabContent.classList.remove('block-active')
})
document.querySelectorAll(`[data-target="${path}"]`).forEach( function(tabsTarget) {
tabsTarget.classList.add('block-active')
})
})
})
//*** tabs active
let tabsChange = document.querySelectorAll('.select__item')
tabsChange.forEach(button => {
button.addEventListener('click', function () {
tabsChange.forEach(btn => btn.classList.remove('active__tab'))
this.classList.add('active__tab')
})
})
})
let select = function () {
let selectHeader = document.querySelectorAll('.select__header');
let selectItem = document.querySelectorAll('.select__item');
selectHeader.forEach(item => {
item.addEventListener('click', selectToggle)
});
selectItem.forEach(item => {
item.addEventListener('click', selectChoose)
});
function selectToggle() {
this.parentElement.classList.toggle('is-active');
}
function selectChoose() {
let text = this.innerText,
select = this.closest('.partner__select'),
currentText = select.querySelector('.select__current');
currentText.innerText = text;
select.classList.remove('is-active');
}
};
//*** Tabs
select();
Delegation shortens the code.
If you delegate, you shorten the code. Never loop eventlisteners in a container. Use the container instead
I lost 20 lines and made code easier to debug
NOTE: I did not have your HTML so I may have created some errors or logic issues you will need to tackle
const selectChoose = e => {
const tgt = e.target;
let text = tgt.innerText,
select = tgt.closest('.partner__select'),
currentText = select.querySelector('.select__current');
currentText.innerText = text;
select.classList.remove('is-active');
};
const selectToggle = e => e.target.parentElement.classList.toggle('is-active');
window.addEventListener('load', function() {
const container = document.getElementById('container');
container.addEventListener('click', e => {
const tgt = e.target.closest('.select');
if (tgt) {
const path = tgt.dataset.path;
document.querySelectorAll('.item', ).forEach(tabContent => tabContent.classList.remove('block-active'))
document.querySelectorAll(`[data-target="${path}"]`).forEach(tabsTarget => tabsTarget.classList.add('block-active'))
}
})
const tabContainer = document.getElementById('tabContainer');
//*** tabs active
tabContainer.addEventListener('click', e => {
const tgt = e.target.closest('button');
if (tgt) {
tabContainer.querySelectorAll('.active__tab').forEach(tab => tabclassList.remove('active__tab'))
tgt.classList.add('active__tab')
}
}) const selContainer = document.getElementById('selectContainer');
selContainer.addEventListener('click', e => {
const tgt = e.target;
if (tgt.classList.contains('select__header')) selectToggle(e);
else if (tgt.classList.contains('select__item')) selectChoose(e)
})
})
I'm trying to change a state of a variable that holds photo ID when a user presses arrow keys or clicks on an image, then I render my image depending on that photo ID.
CODE:
const Lightbox = ({ filteredPhotos }) => {
const [currentPhotoId, setCurrentPhotoId] = useState(null);
const currentPhoto = filteredPhotos.filter((photo) => photo.strapiId === currentPhotoId)[0];
let lightbox = "";
const getLastPhotoId = (filteredPhotos) => {
const ids = filteredPhotos.map((item) => item.strapiId).sort((a, b) => a - b);
const result = ids.slice(-1)[0];
return result;
};
//Select image on click
const selectImage = useCallback(
(e) => {
const divId = parseInt(e.target.parentElement.parentElement.className.split(" ")[0]);
const imgSelected = e.target.parentElement.parentElement.className.includes("gatsby-image-wrapper");
imgSelected && divId !== NaN ? setCurrentPhotoId(divId) : setCurrentPhotoId(null);
},
[setCurrentPhotoId]
);
//Change image on keypress
const changeImage = useCallback(
(e, currentPhotoId) => {
if (document.location.pathname !== "/portfolio" || currentPhotoId === null) return;
const key = e.keyCode;
console.log("changeImage start: ", currentPhotoId);
if (key === 27) {
setCurrentPhotoId(null);
} else if (key === 39) {
setCurrentPhotoId(currentPhotoId + 1);
} else if (key === 37) {
if (currentPhotoId - 1 <= 0) {
setCurrentPhotoId(getLastPhotoId(filteredPhotos))
} else {
setCurrentPhotoId(currentPhotoId - 1)
}
}
},
[setCurrentPhotoId]
);
useEffect(() => {
const gallery = document.getElementById("portfolio-gallery");
gallery.addEventListener("click", (e) => selectImage(e));
document.addEventListener("keydown", (e) => changeImage(e, currentPhotoId));
}, [selectImage, changeImage]);
useEffect(() => {
console.log(currentPhotoId);
}, [currentPhotoId]);
return currentPhotoId === null ? (
<div id="lightbox" className="lightbox">
<h4>Nothing to display</h4>
</div>
) : (
<div id="lightbox" className="lightbox lightbox-active">
<img src={currentPhoto.photo.childImageSharp.fluid.src} alt={currentPhoto.categories[0].name} />
</div>
);
};
export default Lightbox;
Setting up a state by clicking/un-clicking on an image works without a problem, the state is set to a correct number.
But my function that handles keydown events is returned because my state currentPhotoId is null, and I don't get it why when I've set my state by selecting an image.
If I add currentPhotoId in useEffect dependency array
useEffect(() => {
const gallery = document.getElementById("portfolio-gallery");
gallery.addEventListener("click", (e) => selectImage(e));
document.addEventListener("keydown", (e) => changeImage(e, currentPhotoId));
}, [selectImage, changeImage, currentPhotoId]); //here
it breaks my selectImage (click) function. And also the more times user presses right arrow key, the more times it updates the state, resulting into so many updates it crashes down site eventually.
What am I doing wrong? Why my state isn't updating correctly?
FIX:
useEffect(() => {
document.addEventListener("keydown", changeImage); //invoking not calling
return () => {
document.removeEventListener("keydown", changeImage);
};
}, [changeImage, currentPhotoId]);
const changeImage = useCallback(
(e) => {
if (document.location.pathname !== "/portfolio" || currentPhotoId === null) return;
const key = e.keyCode;
if (key === 27) {
setCurrentPhotoId(null);
} else if (key === 39) {
setCurrentPhotoId(currentPhotoId + 1);
} else if (key === 37) {
if (currentPhotoId - 1 <= 0) {
setCurrentPhotoId(getLastPhotoId(filteredPhotos));
} else {
setCurrentPhotoId(currentPhotoId - 1);
}
}
},
[setCurrentPhotoId, currentPhotoId] //added currentPhotoId as dependency
);
So in useEffect I was making a mistake with calling my function, instead of invoking it and this was just adding event listeners to infinite.
And in my callback function instead of passing the state as an argument, I've added id as a dependency.
Also, I've separated selectImage and changeImage into two useEffects, for selectImage useEffect I don't have currentPhotoId as a dependency.
If somebody wants to elaborate more on the details, feel free to do so.
I have an array with the button objects.
When it is clicked, it gets the "button-active" tag and
when it is clicked again, it removes the "button-active" class
I want to removeEventListener when flag is true
let flag = false;
const buttonActive = () => {
arr.forEach(e => {
e.addEventListener("click", function eventListener(event){
event.preventDefault()
if(checkClass(e, "button-active")) removeClass(e, "button-active")
else addClass(e, "button-active")
})
})
}
button.addEventListener("click", (event) => {
event.preventDefault()
let input = document.createElement('div')
input.className = "info"
input.innerHTML += `...(some html with buttons that have weekday class)...`
info.appendChild(input)
flag = true;
arr = document.querySelectorAll('.weekday')
buttonActive()
})
I thought of a way of putting the eventListener function outside the buttonActive function, but the eventListener function uses the variable e.
How should I solve this problem?
simple~
let flag = false;
let cbs = [];
const buttonActive = (arr, active) => {
if (active) {
cbs = arr.map(e => {
const cb = (event) => {
event.preventDefault()
if(checkClass(e, "button-active")) removeClass(e, "button-active")
else addClass(e, "button-active")
}
e.addEventListener("click", cb)
return cb;
});
} else {
for (int i = 0; i < arr.length; ++i) {
arr[i].removeEventListener('click', cbs[i]);
}
}
}
// I guess this is the trigger button
button.addEventListener("click", (event) => {
event.preventDefault()
let input = document.createElement('div')
input.className = "info"
input.innerHTML += `...(some html with buttons that have weekday class)...`
info.appendChild(input)
flag = !!flag;
arr = document.querySelectorAll('.weekday')
buttonActive(arr, flag)
})
I'm having an issue re-rendering items in an array after changes are made to elements in the array. Whether I add by pushing or remove by splicing, when the array is rendered again on the page, it like more items are being added to the array. So if I push onto the array, the item is added, but the old items are then duplicated into the array. Something similar happens when I remove items. The item looks to be removed, but the elements that were in the array show on the page, they are then duplicated and the item that was spliced is gone.
I'm trying to avoid a location.reload('/edit.html') to refresh the page. Kind of cheating. It seems to work, but I'm trying to get the page to refresh with my renderIngredients function. The toggleIngredient function is also duplicating the list of items when I check an item.
import { initializeEditPage, generateLastEdited } from './views'
import { updateRecipe, removeRecipe, saveRecipes, getRecipes, createIngredient } from './recipes'
const titleElement = document.querySelector('#recipe-title')
const bodyElement = document.querySelector('#recipe-body')
const removeElement = document.querySelector('#remove-recipe')
const addElement = document.querySelector('#add-recipe')
const dateElement = document.querySelector('#last-updated')
const addIngredient = document.querySelector('#new-ingredient')
const recipeStatus = document.querySelector('#recipe-status')
const recipeId = location.hash.substring(1)
const recipeOnPage = getRecipes().find((item) => item.id === recipeId)
titleElement.addEventListener('input', (e) => {
const recipe = updateRecipe(recipeId, {
title: e.target.value
})
dateElement.textContent = generateLastEdited(recipe.updatedAt)
})
bodyElement.addEventListener('input', (e) => {
const recipe = updateRecipe(recipeId, {
body: e.target.value
})
dateElement.textContent = generateLastEdited(recipe.updatedAt)
})
addElement.addEventListener('click', () => {
saveRecipes()
location.assign('/index.html')
})
removeElement.addEventListener('click', () => {
removeRecipe(recipeId)
location.assign('/index.html')
})
addIngredient.addEventListener('submit', (e) => {
const text = e.target.elements.text.value.trim()
e.preventDefault()
if (text.length > 0) {
createIngredient(recipeId, text)
e.target.elements.text.value = ''
}
renderIngredients(recipeId)
saveRecipes()
//location.reload('/edit.html')
})
const removeIngredient = (text) => {
const ingredientIndex = recipeOnPage.ingredients.findIndex((ingredient)=> ingredient.text === text)
if (ingredientIndex > -1) {
recipeOnPage.ingredients.splice(ingredientIndex, 1)
}
saveRecipes()
renderIngredients(recipeId)
//location.reload('/edit.html')
}
const toggleIngredient = (text) => {
const ingredient = recipeOnPage.ingredients.find((ingredient) => ingredient.text === text)
if (ingredient.included) {
ingredient.included = false
} else {
ingredient.included = true
}
//location.reload('/edit.html')
}
const ingredientSummary = (recipe) => {
let message
const allUnchecked = recipeOnPage.ingredients.every((ingredient) => ingredient.included === false)
const allChecked = recipeOnPage.ingredients.every((ingredient) => ingredient.included === true)
if (allUnchecked) {
message = `none`
} else if (allChecked) {
message = `all`
} else {
message = `some`
}
return `You have ${message} ingredients for this recipe`
}
const generateIngredientDOM = (ingredient) => {
const ingredientEl = document.createElement('label')
const containerEl = document.createElement('div')
const checkbox = document.createElement('input')
const ingredientText = document.createElement('span')
const removeButton = document.createElement('button')
recipeStatus.textContent = ingredientSummary(recipeOnPage)
// Setup ingredient container
ingredientEl.classList.add('list-item')
containerEl.classList.add('list-item__container')
ingredientEl.appendChild(containerEl)
// Setup ingredient checkbox
checkbox.setAttribute('type', 'checkbox')
checkbox.checked = ingredient.included
containerEl.appendChild(checkbox)
// Create checkbox button in ingredient div
checkbox.addEventListener('click', () => {
toggleIngredient(ingredient.text)
saveRecipes()
renderIngredients(recipeId)
})
// Setup ingredient text
ingredientText.textContent = ingredient.text
containerEl.appendChild(ingredientText)
// Setup the remove button
removeButton.textContent = 'remove'
removeButton.classList.add('button', 'button--text')
ingredientEl.appendChild(removeButton)
// Create remove button in ingredient div
removeButton.addEventListener('click', () => {
removeIngredient(ingredient.text)
saveRecipes()
renderIngredients(recipeId)
})
return ingredientEl
}
const renderIngredients = (recipeId) => {
// Grab the ingredient display from the DOM
const ingredientList = document.querySelector('#ingredients-display')
const recipe = getRecipes().find((item) => {
return item.id === recipeId
})
// Iterate through the list of ingredients on the page and render all items from recipeDOM
recipe.ingredients.forEach((ingredient) => {
const recipeDOM = generateIngredientDOM(ingredient)
ingredientList.appendChild(recipeDOM)
})
}
renderIngredients(recipeId)
I believe the issue stems from my renderIngredients function but I can't figure out how to fix it. Again, when I refresh the page, the results I want display, but I want to avoid using location.reload. I'm expecting the removeIngredient function to remove the ingredient with a button click and the page refreshes with the renderIngredients function. Also expecting the toggleIngredient function to just display a checkbox next to the ingredient I checked off, but that's not what's happening. The Same thing is happening when I use the addIngredient function, the ingredient is being added, but the ingredient that was already on the page is being duplicated.
I guess you want to clear the list before adding the elements again:
ingredientList.innerHTML = "";
How to limit max characters in draft js?
I can get length of the state like that, but how to stop updating component?
var length = editorState.getCurrentContent().getPlainText('').length;
You should define handleBeforeInput and handlePastedText props. In handler-functions, you check the length of current content + length of pasted text and if it reaches the maximum you should return 'handled' string.
UPD 21.03.2018: Upgraded to the last versions of react/react-dom (16.2.0) and Draft.js (0.10.5).
Working example - https://jsfiddle.net/Ln1hads9/11/
const {Editor, EditorState} = Draft;
const MAX_LENGTH = 10;
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty()
};
}
render() {
return (
<div className="container-root">
<Editor
placeholder="Type away :)"
editorState={this.state.editorState}
handleBeforeInput={this._handleBeforeInput}
handlePastedText={this._handlePastedText}
onChange={this._handleChange}
/>
</div>
);
}
_getLengthOfSelectedText = () => {
const currentSelection = this.state.editorState.getSelection();
const isCollapsed = currentSelection.isCollapsed();
let length = 0;
if (!isCollapsed) {
const currentContent = this.state.editorState.getCurrentContent();
const startKey = currentSelection.getStartKey();
const endKey = currentSelection.getEndKey();
const startBlock = currentContent.getBlockForKey(startKey);
const isStartAndEndBlockAreTheSame = startKey === endKey;
const startBlockTextLength = startBlock.getLength();
const startSelectedTextLength = startBlockTextLength - currentSelection.getStartOffset();
const endSelectedTextLength = currentSelection.getEndOffset();
const keyAfterEnd = currentContent.getKeyAfter(endKey);
console.log(currentSelection)
if (isStartAndEndBlockAreTheSame) {
length += currentSelection.getEndOffset() - currentSelection.getStartOffset();
} else {
let currentKey = startKey;
while (currentKey && currentKey !== keyAfterEnd) {
if (currentKey === startKey) {
length += startSelectedTextLength + 1;
} else if (currentKey === endKey) {
length += endSelectedTextLength;
} else {
length += currentContent.getBlockForKey(currentKey).getLength() + 1;
}
currentKey = currentContent.getKeyAfter(currentKey);
};
}
}
return length;
}
_handleBeforeInput = () => {
const currentContent = this.state.editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length;
const selectedTextLength = this._getLengthOfSelectedText();
if (currentContentLength - selectedTextLength > MAX_LENGTH - 1) {
console.log('you can type max ten characters');
return 'handled';
}
}
_handlePastedText = (pastedText) => {
const currentContent = this.state.editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length;
const selectedTextLength = this._getLengthOfSelectedText();
if (currentContentLength + pastedText.length - selectedTextLength > MAX_LENGTH) {
console.log('you can type max ten characters');
return 'handled';
}
}
_handleChange = (editorState) => {
this.setState({ editorState });
}
}
ReactDOM.render(<Container />, document.getElementById('react-root'))
Mikhail's methods are correct, but the handler return value is not. 'not_handled' is a fall-through case that allows the Editor component to process the input normally. In this case, we want to stop the Editor from processing input.
In older versions of DraftJS, it looks like the presence of a string evaluated to 'true' in the handling code, and so the above code behaved correctly. In later versions of DraftJS, the above fiddle doesn't work - I don't have the reputation to post more that one Fiddle here, but try Mikhail's code with v0.10 of DraftJS to replicate.
To correct this, return 'handled' or true when you don't want the Editor to continue handling the input.
Fiddle with corrected return values
For example,
_handleBeforeInput = () => {
const currentContent = this.state.editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length
if (currentContentLength > MAX_LENGTH - 1) {
console.log('you can type max ten characters');
return 'handled';
}
}
See the DraftJS docs on Cancelable Handlers for more.
This is a bit of an old thread, but thought I would share a solution for anyone else facing the problem of character limit and behaviour while pasting text...
Put together something pretty quickly based on the above code by Mikhail to handle this use case which works for me - although I haven't done any work on trying to optimise it.
Basically the handle pasted text looks like so:
const __handlePastedText = (pastedText: any) => {
const currentContent = editorState.getCurrentContent();
const currentContentLength = currentContent.getPlainText('').length;
const selectedTextLength = _getLengthOfSelectedText();
if (currentContentLength + pastedText.length - selectedTextLength > MAX_LENGTH) {
const selection = editorState.getSelection()
const isCollapsed = selection.isCollapsed()
const tempEditorState = !isCollapsed ? _removeSelection() : editorState
_addPastedContent(pastedText, tempEditorState)
return 'handled';
}
return 'not-handled'
}
We have a helper function to handle the deletion of the selection prior to pasting new characters which returns the new editor state:
const _removeSelection = () => {
const selection = editorState.getSelection()
const startKey = selection.getStartKey()
const startOffset = selection.getStartOffset()
const endKey = selection.getEndKey()
const endOffset = selection.getEndOffset()
if (startKey !== endKey || startOffset !== endOffset) {
const newContent = Modifier.removeRange(editorState.getCurrentContent(), selection, 'forward')
const tempEditorState = EditorState.push(
editorState,
newContent,
"remove-range"
)
setEditorState(
tempEditorState
)
return tempEditorState
}
return editorState
}
and finally the function to add the pasted text with a limit:
const _addPastedContent = (input: any, editorState: EditorState) => {
const inputLength = editorState
.getCurrentContent()
.getPlainText().length;
let remainingLength = MAX_LENGTH - inputLength;
const newContent = Modifier.insertText(
editorState.getCurrentContent(),
editorState.getSelection(),
input.slice(0,remainingLength)
);
setEditorState(
EditorState.push(
editorState,
newContent,
"insert-characters"
)
)
}
Link to worked example:
https://codesandbox.io/s/objective-bush-1h9x6
As Mikhail mentioned, you need to handle typing and pasting text. Here are both handlers. Note the paste handler will preserve text that is not outside the limit
function handleBeforeInput(text: string, state: EditorState): DraftHandleValue {
const totalLength = state.getCurrentContent().getPlainText().length + text.length;
return totalLength > MAX_LENGTH ? 'handled' : 'not-handled';
}
function handlePastedText(text: string, _: string, state: EditorState): DraftHandleValue {
const overflowChars = text.length + state.getCurrentContent().getPlainText().length - MAX_LENGTH;
if (overflowChars > 0) {
if (text.length - overflowChars > 0) {
const newContent = Modifier.insertText(
state.getCurrentContent(),
state.getSelection(),
text.substring(0, text.length - overflowChars)
);
setEditorState(EditorState.push(state, newContent, 'insert-characters'));
}
return 'handled';
} else {
return 'not-handled';
}
}
Let's think about this for a second. What is called to make the changes? Your onChange, right? Good. We also know the length. Correct? We attact the "worker" which is the onChange:
const length = editorState.getCurrentContent().getPlainText('').length;
// Your onChange function:
onChange(editorState) {
const MAX_LENGTH = 10;
const length = editorState.getCurrentContent().getPlainText('').length;
if (length <= MAX_LENGTH) {
this.setState({ editorState }) // or this.setState({ editorState: editorState })
}
} else {
console.log(`Sorry, you've exceeded your limit of ${MAX_LENGTH}`)
}
I have not tried this but my 6th sense says it works just fine.