import { Assertions } from '@ephox/agar'; import { context, describe, it } from '@ephox/bedrock-client'; import { Arr, Fun, Optional } from '@ephox/katamari'; import { PlatformDetection } from '@ephox/sand'; import { Hierarchy, SugarElement } from '@ephox/sugar'; import { assert } from 'chai'; import { CaretPosition } from 'tinymce/core/caret/CaretPosition'; import * as LineReader from 'tinymce/core/caret/LineReader'; import * as ViewBlock from '../../module/test/ViewBlock'; const BreakType = LineReader.BreakType; type LineInfo = LineReader.LineInfo; describe('browser.tinymce.core.caret.LineReader', () => { const viewBlock = ViewBlock.bddSetup(); const browser = PlatformDetection.detect().browser; const isSafari13OrLower = browser.isSafari() && (browser.version.major < 13 || browser.version.major === 13 && browser.version.minor < 1); interface Path { path: number[]; offset: number; } const setHtml = viewBlock.update; const logPositions = (msg: string, positions: CaretPosition[]) => { Arr.each(positions, (pos) => { // eslint-disable-next-line no-console console.log(msg, pos.container(), pos.offset(), pos.getClientRects()); }); }; const getPositionsUntilPreviousLine = (path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const pos = CaretPosition(container.dom, offset); return LineReader.getPositionsUntilPreviousLine(viewBlock.get(), pos); }; const getPositionsUntilNextLine = (path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const pos = CaretPosition(container.dom, offset); return LineReader.getPositionsUntilNextLine(viewBlock.get(), pos); }; const getAbovePositions = (path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const pos = CaretPosition(container.dom, offset); return LineReader.getPositionsAbove(viewBlock.get(), pos); }; const getBelowPositions = (path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const pos = CaretPosition(container.dom, offset); return LineReader.getPositionsBelow(viewBlock.get(), pos); }; const findClosestHorizontalPosition = (positions: CaretPosition[], path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const pos = CaretPosition(container.dom, offset); return LineReader.findClosestHorizontalPosition(positions, pos); }; const assertPositions = (actualPositions: CaretPosition[], expectedPositions: Path[]) => { assert.lengthOf(actualPositions, expectedPositions.length, 'Should be the expected amount of positions'); Arr.each(expectedPositions, (p: Path, i) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), p.path).getOrDie(); Assertions.assertDomEq('Should be the expected container', container, SugarElement.fromDom(actualPositions[i].container())); assert.equal(actualPositions[i].offset(), p.offset, 'Should be the expected offset'); }); }; const assertCaretPositions = (actualPositions: CaretPosition[], expectedPositions: Path[]) => { if (expectedPositions.length !== actualPositions.length) { logPositions('cAssertCaretPositions', actualPositions); } assertPositions(actualPositions, expectedPositions); }; const assertNone = (a: Optional) => { assert.isTrue(a.isNone(), 'Optional return value should be none'); }; const assertLineInfoCaretPositions = (lineInfo: LineInfo, expectedPositions: Path[]) => { const actualPositions = lineInfo.positions; if (expectedPositions.length !== actualPositions.length) { logPositions('cAssertLineInfoCaretPositions', actualPositions); } assertPositions(actualPositions, expectedPositions); }; const assertBreakPositionNone = (linebreak: LineInfo) => { assert.isTrue(linebreak.breakAt.isNone(), 'Should not be a line break position'); }; const assertBreakPosition = (linebreak: LineInfo, path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const breakPos = linebreak.breakAt.getOrDie(); Assertions.assertDomEq('Should be the expected container', container, SugarElement.fromDom(breakPos.container())); assert.equal(breakPos.offset(), offset, 'Should be the expected offset'); }; const assertBreakType = (linebreak: LineInfo, expectedBreakType: LineReader.BreakType) => { const actualBreakType = linebreak.breakType; assert.equal(actualBreakType, expectedBreakType, 'Should be the expected break type'); }; const assertCaretPosition = (posOpt: Optional, path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const pos = posOpt.getOrDie('Needs to return a caret'); Assertions.assertDomEq('Should be the expected container', container, SugarElement.fromDom(pos.container())); assert.equal(pos.offset(), offset, 'Should be the expected offset'); }; const visualCaretCheck = (predicate: (scope: HTMLElement, pos: CaretPosition) => boolean, path: number[], offset: number) => { const container = Hierarchy.follow(SugarElement.fromDom(viewBlock.get()), path).getOrDie(); const pos = CaretPosition(container.dom, offset); return predicate(viewBlock.get(), pos); }; const isAtFirstLine = Fun.curry(visualCaretCheck, LineReader.isAtFirstLine); const isAtLastLine = Fun.curry(visualCaretCheck, LineReader.isAtLastLine); context('getPositionsUntilPreviousLine', () => { it('Should be an empty array of positions and no linebreak', () => { setHtml('

a

'); const lineInfo = getPositionsUntilPreviousLine([ 0, 0 ], 0); assertLineInfoCaretPositions(lineInfo, []); assertBreakType(lineInfo, BreakType.Eol); assertBreakPositionNone(lineInfo); }); it('Should be an array with the first position and second position and no linebreak', () => { setHtml('

ab

'); const lineInfo = getPositionsUntilPreviousLine([ 0, 0 ], 2); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 0 }, { path: [ 0, 0 ], offset: 1 } ]); assertBreakType(lineInfo, BreakType.Eol); assertBreakPositionNone(lineInfo); }); it('Should be an array with one position from the second line and a break on the first line 1
', () => { setHtml('

a
b

'); const lineInfo = getPositionsUntilPreviousLine([ 0, 2 ], 1); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 2 ], offset: 0 } ]); assertBreakType(lineInfo, BreakType.Br); assertBreakPosition(lineInfo, [ 0 ], 1); }); it('Should be an array with one position from the second line and a break on the first line 2
', () => { setHtml('

a
bc

'); const lineInfo = getPositionsUntilPreviousLine([ 0, 2 ], 1); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 2 ], offset: 0 } ]); assertBreakType(lineInfo, BreakType.Br); assertBreakPosition(lineInfo, [ 0 ], 1); }); it('Should be an array with one position from the second line and a break on the first line

', () => { setHtml('

a

b

'); const lineInfo = getPositionsUntilPreviousLine([ 1, 0 ], 1); assertLineInfoCaretPositions(lineInfo, [ { path: [ 1, 0 ], offset: 0 } ]); assertBreakType(lineInfo, BreakType.Block); assertBreakPosition(lineInfo, [ 0, 0 ], 1); }); it('Should be an array with one position from the second line and a break on the first line (wrap)', () => { setHtml('
abc def ghi
'); const lineInfo = getPositionsUntilPreviousLine([ 0, 0 ], 6); if (isSafari13OrLower) { assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 4 }, { path: [ 0, 0 ], offset: 5 } ]); assertBreakType(lineInfo, BreakType.Wrap); assertBreakPosition(lineInfo, [ 0, 0 ], 3); } else { assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 5 } ]); assertBreakType(lineInfo, BreakType.Wrap); assertBreakPosition(lineInfo, [ 0, 0 ], 4); } }); it('Should be an array with zero positions from the second line and a break on the first line', () => { setHtml('
abc def ghi
'); const lineInfo = getPositionsUntilPreviousLine([ 0, 0 ], 5); if (isSafari13OrLower) { assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 4 } ]); assertBreakType(lineInfo, BreakType.Wrap); assertBreakPosition(lineInfo, [ 0, 0 ], 3); } else { assertLineInfoCaretPositions(lineInfo, []); assertBreakType(lineInfo, BreakType.Wrap); assertBreakPosition(lineInfo, [ 0, 0 ], 4); } }); }); context('getPositionsUntilNextLine', () => { it('Should be an empty array of positions and no linebreak', () => { setHtml('

a

'); const lineInfo = getPositionsUntilNextLine([ 0, 0 ], 1); assertLineInfoCaretPositions(lineInfo, []); assertBreakType(lineInfo, BreakType.Eol); assertBreakPositionNone(lineInfo); }); it('Should be an array with the first position and second position and no linebreak', () => { setHtml('

ab

'); const lineInfo = getPositionsUntilNextLine([ 0, 0 ], 0); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 1 }, { path: [ 0, 0 ], offset: 2 } ]); assertBreakType(lineInfo, BreakType.Eol); assertBreakPositionNone(lineInfo); }); it('Should be an array with one position from the first line and a break on the first line
', () => { setHtml('

a
b

'); const lineInfo = getPositionsUntilNextLine([ 0, 0 ], 0); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 1 }, { path: [ 0 ], offset: 1 } ]); assertBreakType(lineInfo, BreakType.Br); assertBreakPosition(lineInfo, [ 0 ], 1); }); it('Should be an array with one position from the first line with input and a break on the first line
', () => { setHtml('


b

'); const lineInfo = getPositionsUntilNextLine([ 0 ], 0); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0 ], offset: 1 } ]); assertBreakType(lineInfo, BreakType.Br); assertBreakPosition(lineInfo, [ 0 ], 1); }); it('Should be an array with one position from the first line and a break on the first line

', () => { setHtml('

a

b

'); const lineInfo = getPositionsUntilNextLine([ 0, 0 ], 0); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 1 } ]); assertBreakType(lineInfo, BreakType.Block); assertBreakPosition(lineInfo, [ 1, 0 ], 0); }); it('Should be an array with one position from the second line and a break on the last line', () => { setHtml('
abc def ghi
'); const lineInfo = getPositionsUntilNextLine([ 0, 0 ], 6); assertLineInfoCaretPositions(lineInfo, [ { path: [ 0, 0 ], offset: 7 } ]); assertBreakType(lineInfo, BreakType.Wrap); assertBreakPosition(lineInfo, [ 0, 0 ], 8); }); it('Should be an array with zero positions from the second line and a break on the last line', () => { setHtml('
abc def ghi
'); const lineInfo = getPositionsUntilNextLine([ 0, 0 ], 7); assertLineInfoCaretPositions(lineInfo, []); assertBreakType(lineInfo, BreakType.Wrap); assertBreakPosition(lineInfo, [ 0, 0 ], 8); }); }); context('isAtFirstLine', () => { it('Should return true at first visual position in paragraph', () => { setHtml('

a

'); const firstLine = isAtFirstLine([ 0, 0 ], 0); assert.isTrue(firstLine, 'Should be true on first position in paragraph'); }); it('Should return true at second visual position in paragraph', () => { setHtml('

a

'); const firstLine = isAtFirstLine([ 0, 0 ], 1); assert.isTrue(firstLine, 'Should be true on second position in paragraph'); }); it('Should return false at second br line in paragraph', () => { setHtml('

a
b

'); const firstLine = isAtFirstLine([ 0, 2 ], 0); assert.isFalse(firstLine, 'Should be false on second line in paragraph'); }); it('Should return false at second pos after br line in paragraph', () => { setHtml('

a
b

'); const firstLine = isAtFirstLine([ 0, 2 ], 1); assert.isFalse(firstLine, 'Should be false on second line in paragraph'); }); it('Should return false at second paragraph', () => { setHtml('

a

b

'); const firstLine = isAtFirstLine([ 1, 0 ], 0); assert.isFalse(firstLine, 'Should be false on second line in paragraph'); }); it('Should return false at second line in a wrapped element', () => { setHtml('
abc def ghi
'); const firstLine = isAtFirstLine([ 0, 0 ], 4); assert.isFalse(firstLine, 'Should be false on second line in paragraph'); }); it('Should return true at paragraph in td', () => { setHtml('

a

'); const firstLine = isAtFirstLine([ 0, 0, 0, 0, 0, 0 ], 0); assert.isTrue(firstLine, 'Should be true since it is the first line in td'); }); it('Should return false at second paragraph in td', () => { setHtml('

a

b

'); const firstLine = isAtFirstLine([ 0, 0, 0, 0, 1, 0 ], 0); assert.isFalse(firstLine, 'Should be false since it is the second line in td'); }); }); context('isAtLastLine', () => { it('Should return true at first visual position in paragraph', () => { setHtml('

a

'); const lastLine = isAtLastLine([ 0, 0 ], 0); assert.isTrue(lastLine, 'Should be true on first position in paragraph'); }); it('Should return true at second visual position in paragraph', () => { setHtml('

a

'); const lastLine = isAtLastLine([ 0, 0 ], 1); assert.isTrue(lastLine, 'Should be true on second position in paragraph'); }); it('Should return false at before first br line in paragraph', () => { setHtml('

a
b

'); const lastLine = isAtLastLine([ 0, 0 ], 0); assert.isFalse(lastLine, 'Should be false on first line in paragraph'); }); it('Should return false at first line at second pos before br line in paragraph', () => { setHtml('

a
b

'); const lastLine = isAtLastLine([ 0, 0 ], 1); assert.isFalse(lastLine, 'Should be false on first line in paragraph'); }); it('Should return false at first paragraph', () => { setHtml('

a

b

'); const lastLine = isAtLastLine([ 0, 0 ], 0); assert.isFalse(lastLine, 'Should be false on first paragraph line'); }); it('Should return false at second line in a wrapped element', () => { setHtml('
abc def ghi
'); const lastLine = isAtLastLine([ 0, 0 ], 6); assert.isFalse(lastLine, 'Should be false on second line in paragraph'); }); it('Should return false at first paragraph in td', () => { setHtml('

a

b

'); const lastLine = isAtLastLine([ 0, 0, 0, 0, 0, 0 ], 0); assert.isFalse(lastLine, 'Should be false since it is the first line in td'); }); it('Should return true at second paragraph in td', () => { setHtml('

a

b

'); const lastLine = isAtLastLine([ 0, 0, 0, 0, 1, 0 ], 0); assert.isTrue(lastLine, 'Should be true since it is the second line in td'); }); }); context('getAbovePositions', () => { it('Should return zero positions since there is no line above', () => { setHtml('

a

'); const positions = getAbovePositions([ 0, 0 ], 1); assertCaretPositions(positions, []); }); it('Should return three positions for the line above', () => { setHtml('

ab

a

'); const positions = getAbovePositions([ 1, 0 ], 0); assertCaretPositions(positions, [ { path: [ 0, 0 ], offset: 0 }, { path: [ 0, 0 ], offset: 1 }, { path: [ 0, 0 ], offset: 2 } ]); }); it('Should return four positions for the line above2', () => { setHtml('

ab

a

'); const positions = getAbovePositions([ 1, 0 ], 0); assertCaretPositions(positions, [ { path: [ 0, 0 ], offset: 0 }, { path: [ 0, 0 ], offset: 1 }, { path: [ 0 ], offset: 1 }, { path: [ 0 ], offset: 2 }, { path: [ 0, 2 ], offset: 0 }, { path: [ 0, 2 ], offset: 1 } ]); }); }); context('getBelowPositions', () => { it('Should return zero positions since there is no line below', () => { setHtml('

a

'); const positions = getBelowPositions([ 0, 0 ], 0); assertCaretPositions(positions, []); }); it('Should return three positions for the line below', () => { setHtml('

a

ab

'); const positions = getBelowPositions([ 0, 0 ], 0); assertCaretPositions(positions, [ { path: [ 1, 0 ], offset: 0 }, { path: [ 1, 0 ], offset: 1 }, { path: [ 1, 0 ], offset: 2 } ]); }); it('Should return five positions for the line below', () => { setHtml('

a

ab

'); const positions = getBelowPositions([ 0, 0 ], 0); assertCaretPositions(positions, [ { path: [ 1, 0 ], offset: 0 }, { path: [ 1, 0 ], offset: 1 }, { path: [ 1 ], offset: 1 }, { path: [ 1 ], offset: 2 }, { path: [ 1, 2 ], offset: 0 }, { path: [ 1, 2 ], offset: 1 } ]); }); }); context('findClosestHoriontalPosition (above)', () => { it('Should not return a position since there is no above positions', () => { setHtml('

ab

'); const positions = getAbovePositions([ 0, 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0, 0 ], 0); assertNone(closestPosOpt); }); it('Should return first caret position on the line above', () => { setHtml('

ab

cd

'); const positions = getAbovePositions([ 1, 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 1, 0 ], 0); assertCaretPosition(closestPosOpt, [ 0, 0 ], 0); }); it('Should return last caret position on the line above', () => { setHtml('

ab

cd

'); const positions = getAbovePositions([ 1, 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 1, 0 ], 2); assertCaretPosition(closestPosOpt, [ 0, 0 ], 2); }); it('Should return first indexed caret position on the line above', () => { setHtml('

'); const positions = getAbovePositions([ 1 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 1 ], 0); assertCaretPosition(closestPosOpt, [ 0 ], 0); }); it('Should return last indexed caret position on the line above', () => { setHtml('

'); const positions = getAbovePositions([ 1 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 1 ], 1); assertCaretPosition(closestPosOpt, [ 0 ], 1); }); it('Should return last text node position at the line above', () => { setHtml('

ab

ab

'); const positions = getAbovePositions([ 1, 2 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 1, 2 ], 0); assertCaretPosition(closestPosOpt, [ 0, 2 ], 0); }); }); context('findClosestHorizontalPosition (below)', () => { it('Should not return a position since there is no below positions', () => { setHtml('

ab

'); const positions = getBelowPositions([ 0, 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0, 0 ], 0); assertNone(closestPosOpt); }); it('Should return first caret position on the line below', () => { setHtml('

ab

cd

'); const positions = getBelowPositions([ 0, 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0, 0 ], 0); assertCaretPosition(closestPosOpt, [ 1, 0 ], 0); }); it('Should return last caret position on the line below', () => { setHtml('

ab

cd

'); const positions = getBelowPositions([ 0, 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0, 0 ], 2); assertCaretPosition(closestPosOpt, [ 1, 0 ], 2); }); it('Should return first indexed caret position on the line below', () => { setHtml('

'); const positions = getBelowPositions([ 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0 ], 0); assertCaretPosition(closestPosOpt, [ 1 ], 0); }); it('Should return last indexed caret position on the line below', () => { setHtml('

'); const positions = getBelowPositions([ 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0 ], 1); assertCaretPosition(closestPosOpt, [ 1 ], 1); }); it('Should return first text node position at the line below', () => { setHtml('

ab

ab

'); const positions = getBelowPositions([ 0, 0 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0, 0 ], 0); assertCaretPosition(closestPosOpt, [ 1, 0 ], 0); }); it('Should return last text node position at the line below', () => { setHtml('

ab

ab

'); const positions = getBelowPositions([ 0, 2 ], 0); const closestPosOpt = findClosestHorizontalPosition(positions, [ 0, 2 ], 0); assertCaretPosition(closestPosOpt, [ 1, 2 ], 0); }); }); });