Skip to content

Commit

Permalink
add: Enables to ctrl+z in the TodoEditor.
Browse files Browse the repository at this point in the history
  • Loading branch information
ujiro99 committed Jun 19, 2023
1 parent 5ff845c commit 5c9d8f4
Showing 1 changed file with 145 additions and 56 deletions.
201 changes: 145 additions & 56 deletions src/components/TodoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import { useTaskManager } from '@/hooks/useTaskManager'
import { useStorageWatcher } from '@/hooks/useStorageWatcher'
import { useTaskRecordKey } from '@/hooks/useTaskRecordKey'
import { useEventAlarm } from '@/hooks/useEventAlarm'
import { useAnalytics } from '@/hooks/useAnalytics'
import { Task } from '@/models/task'
import { Group } from '@/models/group'
import { LoadingIcon } from '@/components/LoadingIcon'
import { Icon } from '@/components/Icon'
import { sleep, getIndent, depthToIndent, getIndentDepth } from '@/services/util'
import {
sleep,
getIndent,
depthToIndent,
getIndentDepth,
} from '@/services/util'
import * as i18n from '@/services/i18n'
import { INDENT_SIZE, KEY, KEYCODE_ENTER, DEFAULT } from '@/const'
import { INDENT_SIZE, KEY, KEYCODE_ENTER, TASK_DEFAULT } from '@/const'
import Log from '@/services/log'

const INDENT = depthToIndent(1)
Expand All @@ -32,6 +38,7 @@ export function TodoEditor(): JSX.Element {
const { currentKey } = useTaskRecordKey()
const { fixEventLines } = useEventAlarm()
const inputArea = useRef<HTMLTextAreaElement>()
const analytics = useAnalytics()

useEffect(() => {
setText(manager.getText())
Expand All @@ -52,6 +59,7 @@ export function TodoEditor(): JSX.Element {
}, 1 * 500 /* ms */)

setTimeoutID(newTimeoutId)

return () => {
unmounted = true
clearTimeout(timeoutID)
Expand Down Expand Up @@ -85,65 +93,155 @@ export function TodoEditor(): JSX.Element {
fixEventLines(newRoot)
}

const indent = (indentCount: number, from: number, to: number): string => {
/**
* Add/Remove indent.
* @param depth Indent depth.
* @param from Start row.
* @param to End row.
* @returns changed lines.
*/
const changeIndent = (
depth: number,
from: number,
to: number,
): [string, number, number] => {
const lines = text.split(/\n/)
const indent = ''.padStart(Math.abs(indentCount) * INDENT_SIZE, INDENT)
for (let i = from; i <= to; i++) {
if (indentCount > 0) {
// insert indent.
lines[i] = indent + lines[i]
const chagnedLines = []
const increse = depth > 0
depth = Math.abs(depth)
let fromMoved = 0
let toMoved = 0
for (let i = from - 1; i <= to - 1; i++) {
if (increse) {
// Increase indent.
const indent = depthToIndent(depth)
chagnedLines.push(indent + lines[i])
if (i === from - 1) fromMoved = indent.length
toMoved += indent.length
} else {
// remove indent.
// Decrease indent.
const currentLine = lines[i]
if (currentLine.startsWith(indent)) {
lines[i] = currentLine.slice(Math.abs(indentCount) * INDENT_SIZE)
const currentDepth = getIndentDepth(currentLine)
let d = depth
if (currentDepth === 0) {
d = 0
} else if (currentDepth < depth) {
d = currentDepth
}
chagnedLines.push(currentLine.slice(INDENT_SIZE * d))
if (i === from - 1) fromMoved -= INDENT_SIZE * d
toMoved -= INDENT_SIZE * d
}
}
return lines.join('\n')
return [chagnedLines.join('\n'), fromMoved, toMoved]
}

/**
* Calculate the start position of the row.
* @param row Target row number.
* @param lines
* @returns Start position of the row
*/
const calcRowStartPos = (row: number, lines: string[]): number => {
if (row === 1) return 0
return lines.slice(0, row - 1).join('\n').length + 1
}

/**
* Calculate the end position of the row.
* @param row Target row number.
* @param lines
* @returns End position of the row.
*/
const calcRowEndPos = (row: number, lines: string[]): number => {
let nl = 1
if (row === 1) nl--
return (
lines.slice(0, row - 1).join('\n').length + lines[row - 1].length + nl
)
}

/**
* Set text at selected rows.
* @param from Selected start row nubmer.
* @param to Selected end row number.
* @param newText Text to be set.
* @param appendRow If true, append a new row at the end of the selected rows.
*/
function setTextAtRow(
from: number,
to: number,
newText: string,
appendRow = false,
) {
const lines = text.split(/\n/)
if (appendRow) {
newText = '\n' + newText
const start = calcRowEndPos(from, lines)
const end = calcRowEndPos(to, lines)
inputArea.current.setSelectionRange(start, end)
} else {
const start = calcRowStartPos(from, lines)
const end = calcRowEndPos(to, lines)
inputArea.current.setSelectionRange(start, end)
}

let executed = true
try {
if (!document.execCommand('insertText', false, newText)) {
executed = false
}
} catch (e) {
analytics.track('ErrorInsertText')
Log.e('error caught:', e)
executed = false
}
if (!executed) {
Log.e('insertText unsuccessful, execCommand not supported')
}
}

function onKeyDown(e: React.KeyboardEvent) {
if (e.keyCode === KEYCODE_ENTER) {
// KeyCode is used to distinguish it from the Enter key input during IME
const start = inputArea.current.selectionStart
const end = inputArea.current.selectionEnd
const endChar = text.charAt(end)
if (start !== end) return
if (endChar !== '\n' && text.length !== end) return
if (e.shiftKey) return

const lines = text.split(/\n/)
let currentRow = text.slice(0, start).split(/\n/).length - 1
const currentLine = lines[currentRow]
let currentRow = text.slice(0, start).split(/\n/).length
const currentLine = lines[currentRow - 1]
if (Task.isEmptyTask(currentLine)) {
// Decrease the indent of the current line.
const depth = getIndentDepth(currentLine)
let replaceLine = ""
if (depth > 0) {
replaceLine = depthToIndent(Math.max(depth - 1, 0)) + DEFAULT
// Decrease the indent of the current line.
let replaceLine = depthToIndent(Math.max(depth - 1, 0)) + TASK_DEFAULT
setTextAtRow(currentRow, currentRow, replaceLine)
} else {
// Noting to do.
return
}
lines.splice(currentRow, 1, replaceLine)
currentRow += 1
} else if (Task.isTaskStr(currentLine)) {
// Add a new task as sibling level.
const addedLine = getIndent(currentLine) + DEFAULT
lines.splice(currentRow + 1, 0, addedLine)
currentRow += 2
const addedLine = getIndent(currentLine) + TASK_DEFAULT
setTextAtRow(currentRow, currentRow, addedLine, true)
currentRow++
} else if (Group.test(currentLine)) {
// Add a new task as child level.
const addedLine = getIndent(currentLine) + INDENT + DEFAULT
lines.splice(currentRow + 1, 0, addedLine)
currentRow += 2
const addedLine = getIndent(currentLine) + INDENT + TASK_DEFAULT
setTextAtRow(currentRow, currentRow, addedLine, true)
currentRow++
} else {
// Add a empty line.
lines.splice(currentRow + 1, 0, "")
currentRow += 2
// Add a empty line (default).
return
}

setText(lines.join('\n'))
const newRow = lines.slice(0, currentRow).join('\n').length
setSelection({ start: newRow, end: newRow })
const newPos = inputArea.current.value
.split(/\n/)
.slice(0, currentRow)
.join('\n').length
setSelection({ start: newPos, end: newPos })

e.preventDefault()
return
Expand All @@ -152,30 +250,21 @@ export function TodoEditor(): JSX.Element {
switch (e.code) {
case KEY.TAB: {
const start = inputArea.current.selectionStart
const from = text.slice(0, start).split(/\n/).length - 1
const from = text.slice(0, start).split(/\n/).length
const end = inputArea.current.selectionEnd
const to = text.slice(0, end).split(/\n/).length - 1

let newText: string
if (e.shiftKey) {
newText = indent(-1, from, to)

// Ensure that the rows in the selection area do not change.
const newLines = newText.split(/\n/)
const fromMin = newLines.slice(0, from).join('\n').length + 1
const newFrom = Math.max(fromMin, start - INDENT_SIZE)
const toMin = newLines.slice(0, to).join('\n').length + 1
const newEnd = Math.max(toMin, end - INDENT_SIZE * (to - from + 1))
setSelection({ start: newFrom, end: newEnd })
} else {
newText = indent(1, from, to)
setSelection({
start: start + INDENT_SIZE,
end: end + INDENT_SIZE * (to - from + 1),
})
}
const to = text.slice(0, end).split(/\n/).length

let [newText, fromMoved, toMoved] = changeIndent(
e.shiftKey ? -1 : 1,
from,
to,
)
setTextAtRow(from, to, newText)
setSelection({
start: start + fromMoved,
end: end + toMoved,
})

setText(newText)
e.preventDefault()
break
}
Expand All @@ -198,8 +287,8 @@ export function TodoEditor(): JSX.Element {
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
value={text}
ref={inputArea}
defaultValue={text}
></TextareaAutosize>
</div>
)
Expand Down

0 comments on commit 5c9d8f4

Please sign in to comment.