mirror of
https://github.com/heyman/heynote.git
synced 2025-06-27 13:01:51 +02:00
Merge pull request #204 from wolimst/feat/move-block
Add feature for moving the current block up and down
This commit is contained in:
commit
74558769e0
@ -9,6 +9,7 @@ Here are the most notable changes in each release. For a more detailed list of c
|
||||
- Added support for custom key bindings. See [the documentation](https://heynote.com/docs/#user-content-custom-key-bindings) for more info.
|
||||
- Added a "command palette" that can be accessed by pressing `Ctrl/Cmd+Shift+P`, or just typing `>` in the buffer selector. The command palette allows you to discover all available commands in the app, and to quickly execute them.
|
||||
- Added support for configuring the tab size.
|
||||
- Added functionality for moving blocks up and down. Default key bindings are `Ctrl/Cmd+Alt+Shift+Up` and `Ctrl/Cmd+Alt+Shift+Down`.
|
||||
|
||||
### Other changes
|
||||
|
||||
|
@ -5,6 +5,7 @@ export const LANGUAGE_CHANGE = "heynote-change"
|
||||
export const CURRENCIES_LOADED = "heynote-currencies-loaded"
|
||||
export const SET_CONTENT = "heynote-set-content"
|
||||
export const ADD_NEW_BLOCK = "heynote-add-new-block"
|
||||
export const MOVE_BLOCK = "heynote-move-block"
|
||||
export const DELETE_BLOCK = "heynote-delete-block"
|
||||
export const CURSOR_CHANGE = "heynote-cursor-change"
|
||||
export const APPEND_BLOCK = "heynote-append-block"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { EditorSelection, Transaction } from "@codemirror/state"
|
||||
|
||||
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, DELETE_BLOCK } from "../annotation.js";
|
||||
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, MOVE_BLOCK, DELETE_BLOCK } from "../annotation.js";
|
||||
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./block"
|
||||
import { moveLineDown, moveLineUp } from "./move-lines.js";
|
||||
import { selectAll } from "./select-all.js";
|
||||
@ -322,6 +322,62 @@ export function triggerCurrenciesLoaded(state, dispatch) {
|
||||
}))
|
||||
}
|
||||
|
||||
export function moveCurrentBlockUp({state, dispatch}) {
|
||||
return moveCurrentBlock(state, dispatch, true)
|
||||
}
|
||||
|
||||
export function moveCurrentBlockDown({state, dispatch}) {
|
||||
return moveCurrentBlock(state, dispatch, false)
|
||||
}
|
||||
|
||||
function moveCurrentBlock(state, dispatch, up) {
|
||||
if (state.readOnly) {
|
||||
return false
|
||||
}
|
||||
|
||||
const blocks = state.facet(blockState)
|
||||
const currentBlock = getActiveNoteBlock(state)
|
||||
const blockIndex = blocks.indexOf(currentBlock)
|
||||
if ((up && blockIndex === 0) || (!up && blockIndex === blocks.length - 1)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dir = up ? -1 : 1
|
||||
const neighborBlock = blocks[blockIndex + dir]
|
||||
|
||||
const currentBlockContent = state.sliceDoc(currentBlock.delimiter.from, currentBlock.content.to)
|
||||
const neighborBlockContent = state.sliceDoc(neighborBlock.delimiter.from, neighborBlock.content.to)
|
||||
const newContent = up ? currentBlockContent + neighborBlockContent : neighborBlockContent + currentBlockContent
|
||||
|
||||
const selectionRange = state.selection.asSingle().ranges[0]
|
||||
let newSelectionRange
|
||||
if (up) {
|
||||
newSelectionRange = EditorSelection.range(
|
||||
selectionRange.anchor - currentBlock.delimiter.from + neighborBlock.delimiter.from,
|
||||
selectionRange.head - currentBlock.delimiter.from + neighborBlock.delimiter.from,
|
||||
)
|
||||
} else {
|
||||
newSelectionRange = EditorSelection.range(
|
||||
selectionRange.anchor + neighborBlock.content.to - neighborBlock.delimiter.from,
|
||||
selectionRange.head + neighborBlock.content.to - neighborBlock.delimiter.from,
|
||||
)
|
||||
}
|
||||
|
||||
dispatch(state.update({
|
||||
changes: {
|
||||
from: up ? neighborBlock.delimiter.from : currentBlock.delimiter.from,
|
||||
to: up ? currentBlock.content.to : neighborBlock.content.to,
|
||||
insert: newContent,
|
||||
},
|
||||
selection: newSelectionRange,
|
||||
annotations: [heynoteEvent.of(MOVE_BLOCK)],
|
||||
}, {
|
||||
scrollIntoView: true,
|
||||
userEvent: "input",
|
||||
}))
|
||||
return true
|
||||
}
|
||||
|
||||
export const deleteBlock = (editor) => ({state, dispatch}) => {
|
||||
const range = state.selection.asSingle().ranges[0]
|
||||
const blocks = state.facet(blockState)
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
selectAll,
|
||||
deleteBlock, deleteBlockSetCursorPreviousBlock,
|
||||
newCursorBelow, newCursorAbove,
|
||||
moveCurrentBlockUp, moveCurrentBlockDown,
|
||||
} from "./block/commands.js"
|
||||
import { deleteLine } from "./block/delete-line.js"
|
||||
import { formatBlockContent } from "./block/format-code.js"
|
||||
@ -85,6 +86,8 @@ const HEYNOTE_COMMANDS = {
|
||||
insertNewBlockAtCursor: cmd(insertNewBlockAtCursor, "Block", "Insert new block at cursor"),
|
||||
deleteBlock: cmd(deleteBlock, "Block", "Delete block"),
|
||||
deleteBlockSetCursorPreviousBlock: cmd(deleteBlockSetCursorPreviousBlock, "Block", "Delete block and set cursor to previous block"),
|
||||
moveCurrentBlockUp: cmdLessContext(moveCurrentBlockUp, "Block", "Move current block up"),
|
||||
moveCurrentBlockDown: cmdLessContext(moveCurrentBlockDown, "Block", "Move current block down"),
|
||||
cursorPreviousBlock: cmd(cursorPreviousBlock, "Cursor", "Move cursor to previous block"),
|
||||
cursorNextBlock: cmd(cursorNextBlock, "Cursor", "Move cursor to next block"),
|
||||
cursorPreviousParagraph: cmd(cursorPreviousParagraph, "Cursor", "Move cursor to previous paragraph"),
|
||||
|
@ -40,6 +40,8 @@ export const DEFAULT_KEYMAP = [
|
||||
...cmdShift("PageDown", "cursorPageDown", "selectPageDown"),
|
||||
...cmdShift("Home", "cursorLineBoundaryBackward", "selectLineBoundaryBackward"),
|
||||
...cmdShift("End", "cursorLineBoundaryForward", "selectLineBoundaryForward"),
|
||||
cmd("Alt-Mod-Shift-ArrowUp", "moveCurrentBlockUp"),
|
||||
cmd("Alt-Mod-Shift-ArrowDown", "moveCurrentBlockDown"),
|
||||
cmd("Backspace", "deleteCharBackward"),
|
||||
cmd("Delete", "deleteCharForward"),
|
||||
cmd("Escape", "simplifySelection"),
|
||||
|
125
tests/move-block-between-buffers.spec.js
Normal file
125
tests/move-block-between-buffers.spec.js
Normal file
@ -0,0 +1,125 @@
|
||||
import {expect, test} from "@playwright/test";
|
||||
import {HeynotePage} from "./test-utils.js";
|
||||
|
||||
import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js"
|
||||
import { NoteFormat } from "../src/common/note-format.js"
|
||||
|
||||
|
||||
let heynotePage
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
heynotePage = new HeynotePage(page)
|
||||
await heynotePage.goto()
|
||||
|
||||
expect((await heynotePage.getBlocks()).length).toBe(1)
|
||||
await heynotePage.setContent(`
|
||||
∞∞∞text
|
||||
Block A
|
||||
∞∞∞text
|
||||
Block B
|
||||
∞∞∞text
|
||||
Block C`)
|
||||
await page.waitForTimeout(100);
|
||||
// check that blocks are created
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
|
||||
// check that visual block layers are created
|
||||
await expect(page.locator("css=.heynote-blocks-layer > div")).toHaveCount(3)
|
||||
|
||||
// create secondary buffer
|
||||
await heynotePage.saveBuffer("other.txt", `
|
||||
∞∞∞text-a
|
||||
First block
|
||||
∞∞∞math
|
||||
Second block`)
|
||||
});
|
||||
|
||||
|
||||
test("move block to other buffer", async ({page}) => {
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||
|
||||
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||
expect(buffers).toContain("other.txt")
|
||||
|
||||
const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt"))
|
||||
|
||||
expect(await heynotePage.getContent()).toBe(`
|
||||
∞∞∞text
|
||||
Block A
|
||||
∞∞∞text
|
||||
Block B`)
|
||||
|
||||
expect(otherBuffer.content).toBe(`
|
||||
∞∞∞text-a
|
||||
First block
|
||||
∞∞∞math
|
||||
Second block
|
||||
∞∞∞text
|
||||
Block C`)
|
||||
|
||||
})
|
||||
|
||||
|
||||
test("move block to other open/cached buffer", async ({page}) => {
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+P"))
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+P"))
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||
|
||||
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||
expect(buffers).toContain("other.txt")
|
||||
|
||||
const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt"))
|
||||
|
||||
expect(await heynotePage.getContent()).toBe(`
|
||||
∞∞∞text
|
||||
Block A
|
||||
∞∞∞text
|
||||
Block B`)
|
||||
|
||||
expect(otherBuffer.content).toBe(`
|
||||
∞∞∞text-a
|
||||
First block
|
||||
∞∞∞math
|
||||
Second block
|
||||
∞∞∞text
|
||||
Block C`)
|
||||
|
||||
})
|
||||
|
||||
test("cursor position after moving first block", async ({page}) => {
|
||||
await heynotePage.setCursorPosition(10)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(10)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(9)
|
||||
})
|
||||
|
||||
test("cursor position after moving middle block", async ({page}) => {
|
||||
await heynotePage.setCursorPosition(28)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(25)
|
||||
})
|
||||
|
||||
test("cursor position after moving last block", async ({page}) => {
|
||||
await heynotePage.setCursorPosition(48)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(32)
|
||||
})
|
@ -1,9 +1,5 @@
|
||||
import {expect, test} from "@playwright/test";
|
||||
import {HeynotePage} from "./test-utils.js";
|
||||
|
||||
import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js"
|
||||
import { NoteFormat } from "../src/common/note-format.js"
|
||||
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { HeynotePage } from "./test-utils.js"
|
||||
|
||||
let heynotePage
|
||||
|
||||
@ -19,107 +15,97 @@ Block A
|
||||
Block B
|
||||
∞∞∞text
|
||||
Block C`)
|
||||
await page.waitForTimeout(100);
|
||||
// check that blocks are created
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
|
||||
// check that visual block layers are created
|
||||
await expect(page.locator("css=.heynote-blocks-layer > div")).toHaveCount(3)
|
||||
|
||||
// create secondary buffer
|
||||
await heynotePage.saveBuffer("other.txt", `
|
||||
∞∞∞text-a
|
||||
First block
|
||||
∞∞∞math
|
||||
Second block`)
|
||||
});
|
||||
|
||||
|
||||
test("move block to other buffer", async ({page}) => {
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||
|
||||
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||
expect(buffers).toContain("other.txt")
|
||||
|
||||
const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt"))
|
||||
|
||||
expect(await heynotePage.getContent()).toBe(`
|
||||
∞∞∞text
|
||||
Block A
|
||||
∞∞∞text
|
||||
Block B`)
|
||||
|
||||
expect(otherBuffer.content).toBe(`
|
||||
∞∞∞text-a
|
||||
First block
|
||||
∞∞∞math
|
||||
Second block
|
||||
∞∞∞text
|
||||
Block C`)
|
||||
|
||||
})
|
||||
|
||||
test("move the first block up", async ({ page }) => {
|
||||
// select the first block, cursor position: "Block A|"
|
||||
await page.locator("body").press("ArrowUp")
|
||||
await page.locator("body").press("ArrowUp")
|
||||
|
||||
test("move block to other open/cached buffer", async ({page}) => {
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+P"))
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+P"))
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||
|
||||
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||
expect(buffers).toContain("other.txt")
|
||||
|
||||
const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt"))
|
||||
|
||||
expect(await heynotePage.getContent()).toBe(`
|
||||
∞∞∞text
|
||||
Block A
|
||||
∞∞∞text
|
||||
Block B`)
|
||||
|
||||
expect(otherBuffer.content).toBe(`
|
||||
∞∞∞text-a
|
||||
First block
|
||||
∞∞∞math
|
||||
Second block
|
||||
∞∞∞text
|
||||
Block C`)
|
||||
await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowUp`)
|
||||
const cursorPosition = await heynotePage.getCursorPosition()
|
||||
const content = await heynotePage.getContent()
|
||||
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("Block A")
|
||||
expect(await heynotePage.getBlockContent(1)).toBe("Block B")
|
||||
expect(await heynotePage.getBlockContent(2)).toBe("Block C")
|
||||
expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("A")
|
||||
})
|
||||
|
||||
test("cursor position after moving first block", async ({page}) => {
|
||||
await heynotePage.setCursorPosition(10)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(10)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(9)
|
||||
test("move the middle block up", async ({ page }) => {
|
||||
// select the second block, cursor position: "Block B|"
|
||||
await page.locator("body").press("ArrowUp")
|
||||
|
||||
await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowUp`)
|
||||
const cursorPosition = await heynotePage.getCursorPosition()
|
||||
const content = await heynotePage.getContent()
|
||||
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("Block B")
|
||||
expect(await heynotePage.getBlockContent(1)).toBe("Block A")
|
||||
expect(await heynotePage.getBlockContent(2)).toBe("Block C")
|
||||
expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("B")
|
||||
})
|
||||
|
||||
test("cursor position after moving middle block", async ({page}) => {
|
||||
await heynotePage.setCursorPosition(28)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(25)
|
||||
test("move the last block up", async ({ page }) => {
|
||||
// cursor position: "Block C|"
|
||||
await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowUp`)
|
||||
const cursorPosition = await heynotePage.getCursorPosition()
|
||||
const content = await heynotePage.getContent()
|
||||
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("Block A")
|
||||
expect(await heynotePage.getBlockContent(1)).toBe("Block C")
|
||||
expect(await heynotePage.getBlockContent(2)).toBe("Block B")
|
||||
expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("C")
|
||||
})
|
||||
|
||||
test("cursor position after moving last block", async ({page}) => {
|
||||
await heynotePage.setCursorPosition(48)
|
||||
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||
await page.waitForTimeout(50)
|
||||
await page.locator("body").press("Enter")
|
||||
await page.waitForTimeout(50)
|
||||
expect(await heynotePage.getCursorPosition()).toBe(32)
|
||||
test("move the first block down", async ({ page }) => {
|
||||
// select the first block, cursor position: "Block A|"
|
||||
await page.locator("body").press("ArrowUp")
|
||||
await page.locator("body").press("ArrowUp")
|
||||
|
||||
await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowDown`)
|
||||
const cursorPosition = await heynotePage.getCursorPosition()
|
||||
const content = await heynotePage.getContent()
|
||||
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("Block B")
|
||||
expect(await heynotePage.getBlockContent(1)).toBe("Block A")
|
||||
expect(await heynotePage.getBlockContent(2)).toBe("Block C")
|
||||
expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("A")
|
||||
})
|
||||
|
||||
test("move the middle block down", async ({ page }) => {
|
||||
// select the second block, cursor position: "Block B|"
|
||||
await page.locator("body").press("ArrowUp")
|
||||
|
||||
await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowDown`)
|
||||
const cursorPosition = await heynotePage.getCursorPosition()
|
||||
const content = await heynotePage.getContent()
|
||||
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("Block A")
|
||||
expect(await heynotePage.getBlockContent(1)).toBe("Block C")
|
||||
expect(await heynotePage.getBlockContent(2)).toBe("Block B")
|
||||
expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("B")
|
||||
})
|
||||
|
||||
test("move the last block down", async ({ page }) => {
|
||||
// cursor position: "Block C|"
|
||||
await page.locator("body").press(`${heynotePage.isMac ? "Meta" : "Control"}+Shift+Alt+ArrowDown`)
|
||||
const cursorPosition = await heynotePage.getCursorPosition()
|
||||
const content = await heynotePage.getContent()
|
||||
|
||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||
expect(await heynotePage.getBlockContent(0)).toBe("Block A")
|
||||
expect(await heynotePage.getBlockContent(1)).toBe("Block B")
|
||||
expect(await heynotePage.getBlockContent(2)).toBe("Block C")
|
||||
expect(content.slice(cursorPosition - 1, cursorPosition)).toBe("C")
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user