Update 6 Oct 2020: Changed filtering buttons logic to work properly for tasks without emoji, but with attributes in preview (for example, with time).
I have several repeting tasks in Notion Calendar. Such as zero-inbox and other routine stuff that I need to do every once in a while. Notion doesn't have a feature like Google Calendar that would enable us to create those tasks as re-occuring events. I end up copying tasks for the day and moving them to the next week manually. It works fine for tasks emojis and properties, but every copy has an appended 'Copy of' in it's title. Removing it manually is exhausting and boring. Let's automate it!
Notion API is not released yet. To automate text replacement in titles I played around with Notion in browser. The resulting script is a bit time consuming, but it is still better than manual editing. Here's how I wrote it.
We need to list all events that start with 'Copy of'. One way to do that is to open Calendar view and filter out buttons. Every button has task's title in HTML, so we can use it. We would need buttons later to open modals and edit titles.
function getButtons(regex) {
return Array.from(document.querySelectorAll('.notion-calendar-view .notion-collection-item a > div:first-child')).filter(node => {
const text = node.innerText.split(/\n/)
return text.some(title => regex.test(title))
})
}
getButtons(/^Copy of /)We don't want to touch original tasks, so we filter out buttons for the tasks that start with 'Copy of '. Each node's innerText will start either with emoji (if you set up emoji for the task) or with text. To keep things simple, we would like to test regex against actual title, not emoji. In case emoji exists, it is separated from text with a new line, so we use split here.
// Update 6 Oct 2020 //
We can't simply rely on text.length to find title as it was in previous version. There could be a task without emoji but with time or other attributes listed on new lines after title. So the length is not sufficient for finding index of title in text array. We can test each entry (or line) of text and filter those buttons which contain at least one line that is tested positive.
It has a downside: if some attribute's value is listed in preview (in button's text) and starts with 'Copy of ', we'll end up with filtering more buttons than we might need and the task will take longer. For my case it's not an issue, but if your regex is something other than 'Copy of ', you might want to find a better solution.
//-------------------//
To open modal in Notion we need to dispatch 3 events: mousedown, mouseup and click
function openModal(button) {
button.dispatchEvent(new Event('mousedown', { bubbles: true }))
button.dispatchEvent(new Event('mouseup', { bubbles: true }))
button.dispatchEvent(new Event('click', { bubbles: true }))
}The title is in contenteditable <div>, so we need to change it's innerHTML and trigger input event.
function removeText(regex) {
let input = document.querySelector('div.vertical:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(3) > div:nth-child(1)')
input.innerHTML = input.innerHTML.replace(regex, '')
input.dispatchEvent(new Event('input', { bubbles: true }))
}After we edit one task we need to close modal and click on the next button on our list.
function closeModal() {
let backdrop = document.querySelector('.notion-peek-renderer > div')
backdrop.dispatchEvent(new Event('click', { bubbles: true }))
}When we click a button, browser needs some time to fetch task's data and render the modal. So we need to wait for it with setTimeout. I tried to wait for different amount of time, 400ms turns out to be not enough, so I settled on 1 second, which worked fine so far. Feel free to adjust it if you need to.
Another caveat here is that we need to work synchoroniously and edit one task after another. Or at least simulate sync code execution. Wrapping our setTimeout in a Promise should do the trick.
function handleModal(regex) {
return new Promise(resolve => {
setTimeout(() => {
removeText(regex)
closeModal()
resolve()
}, 1000)
})
}async function replaceText(regex = /^Copy of /) {
const buttons = getButtons(regex)
for (let button of buttons) {
openModal(button)
await handleModal(regex)
}
}Now call replaceText() and enjoy your coffee while this little ad-hoc macros does the boring renaming for you. ✨
