diff --git a/src/components/TodoEditor.tsx b/src/components/TodoEditor.tsx index a3f10ee..9da5c46 100644 --- a/src/components/TodoEditor.tsx +++ b/src/components/TodoEditor.tsx @@ -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) @@ -32,6 +38,7 @@ export function TodoEditor(): JSX.Element { const { currentKey } = useTaskRecordKey() const { fixEventLines } = useEventAlarm() const inputArea = useRef() + const analytics = useAnalytics() useEffect(() => { setText(manager.getText()) @@ -52,6 +59,7 @@ export function TodoEditor(): JSX.Element { }, 1 * 500 /* ms */) setTimeoutID(newTimeoutId) + return () => { unmounted = true clearTimeout(timeoutID) @@ -85,22 +93,112 @@ 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) { @@ -108,42 +206,42 @@ export function TodoEditor(): JSX.Element { // 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 @@ -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 } @@ -198,8 +287,8 @@ export function TodoEditor(): JSX.Element { onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown} - value={text} ref={inputArea} + defaultValue={text} > )