From 11499f0f6f514fd08418d9bd6801b7f3f7bfc2af Mon Sep 17 00:00:00 2001 From: Chris Lozac'h Date: Sat, 16 May 2020 15:48:43 -0700 Subject: [PATCH 1/5] New Batch Editing features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Batch Linking - Select one or more blocks (full blue highlight…not edit mode) - Press `Meta+shift+l` - Type a word or phrase to batch-convert to [[links]] - Press `Enter` Note: Batch Linking purposefully matches on whole-word boundaries only. This avoides accidentally linking words within other words, so batch-linking `ears` will turn `ears` into `[[ears]]`, but will leave `earshot` alone. This is in contrast to Roam's "Link All" feature, which will happily create a link to your page that looks like this: `[[ears]]hot`. # End Tags - Select one or more blocks (full blue highlight…not edit mode) - Press `Ctrl+shift+t` - Type a word to use as a tag - Press `Enter` The word you typed will be appended to every block, so… • One block • Another block …becomes… • One block #tagged • Another block #tagged # Known bugs - ✅ `tag` appends ` #tag`, but ❌ `urgent tag` appends ` #urgent tag`. - ❌ A number of roam-toolkit behaviors seem to break Undo, and these functions are no exception - ❌ While thse functions preserve the highlighted blocks, making it easy to toggle tags on-and-off, but the highlight is overly-permanent. Workaround: reload the page. --- src/ts/features/batch-editing.ts | 90 ++++++++++++++++++++++++++++++++ src/ts/features/features.ts | 2 + src/ts/roam/roam.ts | 14 +++++ src/ts/utils/async.ts | 10 ++++ src/ts/utils/dom.ts | 12 +++++ 5 files changed, 128 insertions(+) create mode 100644 src/ts/features/batch-editing.ts diff --git a/src/ts/features/batch-editing.ts b/src/ts/features/batch-editing.ts new file mode 100644 index 00000000..f825d49c --- /dev/null +++ b/src/ts/features/batch-editing.ts @@ -0,0 +1,90 @@ +import {Feature, Shortcut} from '../utils/settings' +import {Roam} from '../roam/roam' +import {getHighlightedBlocks} from '../utils/dom' +import {forEachAsync, delay} from '../utils/async' + +export const config: Feature = { + id: 'editing', + name: 'Batch Editing', + settings: [ + { + type: 'shortcut', + id: 'batchLinking', + label: 'Apply [[link]] brackets to a word in every highlighted block\n\n(whole-words only)', + initValue: 'Meta+shift+l', + onPress: () => batchLinking(), + } as Shortcut, + { + type: 'shortcut', + id: 'batchAppendTag', + label: 'Append #yourtag to every highlighted block', + initValue: 'Ctrl+shift+t', + onPress: () => appendTag(), + } as Shortcut, + { + type: 'shortcut', + id: 'removeLastEndTag', + label: 'Remove the last #tag at the end of each highlighted block', + initValue: 'Ctrl+shift+meta+t', + onPress: () => removeLastTag(), + } as Shortcut, + ], +} + +const batchLinking = () => { + const text = prompt('What (whole) word do you want to convert into bracketed links?') + if (!text || text === '') return + + const warning = `Replace all visible occurrences of "${text}" in the highlighted blocks with "[[${text}]]"? + + 🛑 This operation CANNOT BE UNDONE!` + + if (!confirm(warning)) return + + withHighlightedBlocks(originalString => { + // Replace whole words only, ignoring already-[[linked]] matches. + // http://www.rexegg.com/regex-best-trick.html#javascriptcode + const regex = new RegExp(`\\[\\[${text}]]|(\\b${text}\\b)`, 'g') + return originalString.replace(regex, function (m, group1) { + if (!group1) return m + else return `[[${m}]]` + }) + }) +} + +const appendTag = () => { + const text = prompt('What "string" do you want to append as "#string"?') + if (!text || text === '') return + + withHighlightedBlocks(originalString => { + return `${originalString} #${text}` + }) +} + +const removeLastTag = () => { + if (!confirm('Remove the end tag from every highlighted block?')) + return + + withHighlightedBlocks(originalString => { + const regex = new RegExp(`(.*) (#.*)`) + return originalString.replace(regex, '$1') + }) +} + +const withHighlightedBlocks = (mod: { (orig: string): string }) => { + const highlighted = getHighlightedBlocks() + + const contentBlocks = Array.from(highlighted.contentBlocks) + forEachAsync(contentBlocks, async element => { + await Roam.replace(element, mod) + }) + + // Preserve selection + const parentBlocks = Array.from(highlighted.parentBlocks) + forEachAsync(parentBlocks, async element => { + // Wait for dom to settle before re-applying highlight style + await delay(100) + await element.classList.add('block-highlight-blue') + }) +} + diff --git a/src/ts/features/features.ts b/src/ts/features/features.ts index fabb86bd..091cd6ca 100644 --- a/src/ts/features/features.ts +++ b/src/ts/features/features.ts @@ -4,6 +4,7 @@ import {config as incDec} from './inc-dec-value' import {config as customCss} from './custom-css' import {config as srs} from '../srs/srs' import {config as blockManipulation} from './block-manipulation' +import {config as batchEditing} from './batch-editing' import {config as estimate} from './estimates' import {config as navigation} from './navigation' import {filterAsync, mapAsync} from '../utils/async' @@ -13,6 +14,7 @@ export const Features = { incDec, // prettier srs, blockManipulation, + batchEditing, estimate, customCss, navigation, diff --git a/src/ts/roam/roam.ts b/src/ts/roam/roam.ts index daa260d4..418560ac 100644 --- a/src/ts/roam/roam.ts +++ b/src/ts/roam/roam.ts @@ -77,6 +77,20 @@ export const Roam = { this.applyToCurrent(node => node.withCursorAtTheEnd()) }, + async replace(element: HTMLElement, mod: { (orig: string): string }) { + const textarea = await Roam.activateBlock(element) as HTMLTextAreaElement + if (!textarea) { + console.log("🚨 NO TEXTAREA returned from ", element) + return + } + + const newText = mod(textarea.value) + + Roam.save(new RoamNode(newText)) + Keyboard.pressEsc() + Keyboard.pressEsc() + }, + writeText(text: string) { this.applyToCurrent(node => new RoamNode(text, node.selection)) return this.getActiveRoamNode()?.text === text diff --git a/src/ts/utils/async.ts b/src/ts/utils/async.ts index dc414ecf..b7515e52 100644 --- a/src/ts/utils/async.ts +++ b/src/ts/utils/async.ts @@ -16,3 +16,13 @@ export async function filterAsync( const filterMap = await mapAsync(array, callbackfn) return array.filter((_value, index) => filterMap[index]) } + +export async function forEachAsync( + array: T[], + callbackfn: (value: T, ...args: any[]) => void +) { + for await (const element of array) { + await callbackfn(element, arguments) + } +} + diff --git a/src/ts/utils/dom.ts b/src/ts/utils/dom.ts index 35dbd6af..9093d3df 100644 --- a/src/ts/utils/dom.ts +++ b/src/ts/utils/dom.ts @@ -33,6 +33,18 @@ export function getFirstTopLevelBlock() { return firstChild.querySelector('.roam-block, textarea') as HTMLElement } +export function getHighlightedBlocks(): { parentBlocks: NodeListOf, contentBlocks: NodeListOf } { + + const highlightedParentBlocks = document.querySelectorAll('.block-highlight-blue') as NodeListOf + + const highlightedContentBlocks = document.querySelectorAll('.block-highlight-blue .roam-block') as NodeListOf + + return { + parentBlocks: highlightedParentBlocks, + contentBlocks: highlightedContentBlocks + } +} + export function getInputEvent() { return new Event('input', { bubbles: true, From 477129ee5fff740249ec1f19f227d0acf725fd52 Mon Sep 17 00:00:00 2001 From: Chris Lozac'h Date: Sat, 16 May 2020 16:03:22 -0700 Subject: [PATCH 2/5] Updated README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 13413604..b755787d 100644 --- a/README.md +++ b/README.md @@ -20,19 +20,21 @@ It's available in their respective extension stores for both [Chrome](https://ch ![](./media/fuzzy_date.gif) 1. Date increment/decrement + - Shortcuts for ±1 day and ±7 days - If there is only 1 date in the block - place the cursor anywhere withing it and press `Ctrl-Alt-Up/Down`, if there is more then 1 date - you need to place the cursor within the name of the date. 1. Spaced repetition * Anki SRS algorithm & Shortcuts * Leitner System automation shortcuts -1. Block actions: Delete, Duplicate +1. Single-block actions: Duplicate, Delete, Copy Block Reference, and Copy Block Embed +1. Batch-block actions: Batch Link a word or phrase, and add/remove "end" tags 1. Task estimates 1. Custom CSS - +1. Navigation Hotkeys: Go to Today, Go to Tomorrow, and Go to Yesterday ## Running the development version 1. Checkout the repository -2. Revert the https://github.com/roam-unofficial/roam-toolkit/commit/20ad9560b7cfaf71adf65dbc3645b3554c2ab598 change locally to allow Toolkit to properly run in the development mode +2. Revert the https://github.com/roam-unofficial/roam-toolkit/commit/20ad9560b7cfaf71adf65dbc3645b3554c2ab598 change locally to allow Toolkit to properly run in the development mode. **← Do not commit the resulting change as it will break the release build!** ### In terminal or command prompt From f89a5004cb77aebf7cb01a14866f416a43c09abf Mon Sep 17 00:00:00 2001 From: Chris Lozac'h Date: Sat, 16 May 2020 17:02:57 -0700 Subject: [PATCH 3/5] Fix race condition that sometimes causes blocks to get skipped during batch operations --- src/ts/roam/roam.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ts/roam/roam.ts b/src/ts/roam/roam.ts index 418560ac..07c7dba2 100644 --- a/src/ts/roam/roam.ts +++ b/src/ts/roam/roam.ts @@ -2,6 +2,7 @@ import {RoamNode, Selection} from './roam-node' import {getActiveEditElement, getFirstTopLevelBlock, getInputEvent, getLastTopLevelBlock} from '../utils/dom' import {Keyboard} from '../utils/keyboard' import {Mouse} from '../utils/mouse' +import {delay} from '../utils/async' export const Roam = { save(roamNode: RoamNode) { @@ -49,6 +50,12 @@ export const Roam = { async activateBlock(element: HTMLElement) { if (element.classList.contains('roam-block')) { await Mouse.leftClick(element) + + // Prevent race condition when attempting to use the + // resulting block before Roam has had a chance to + // replace the with a