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. ✨