Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Jul 29, 2023
1 parent 93b07d6 commit a899b23
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 97 deletions.
104 changes: 42 additions & 62 deletions core/quill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import logger, { DebugLevel } from './logger';
import Module from './module';
import Selection, { Range } from './selection';
import Composition from './composition';
import { compute } from 'compute-scroll-into-view';
import Theme, { ThemeConstructor } from './theme';

const debug = logger('quill');
Expand Down Expand Up @@ -282,9 +283,11 @@ class Quill {
this.container.classList.toggle('ql-disabled', !enabled);
}

focus() {
focus(options: { preventScroll?: boolean } = {}) {
this.selection.focus();
this.scrollSelectionIntoView();
if (!options.preventScroll) {
this.scrollSelectionIntoView();
}
}

format(
Expand Down Expand Up @@ -611,72 +614,49 @@ class Quill {
);
}

/**
* @deprecated Use Quill#scrollSelectionIntoView() instead.
*/
scrollIntoView() {
console.warn(
'Quill#scrollIntoView() has been deprecated and will be removed in the near future. Please use Quill#scrollSelectionIntoView() instead.',
);
this.scrollSelectionIntoView();
}

/**
* Scroll the current selection into the visible area.
* If the selection is already visible, no scrolling will occur.
*/
scrollSelectionIntoView() {
const range = this.selection.lastRange;
if (range == null) return;
const bounds = this.selection.getBounds(range.index, range.length);
if (bounds == null) return;

let { top, bottom } = bounds;

const { body, defaultView } = this.root.ownerDocument;
if (!defaultView) return;

let targetTop = 0;
let targetBottom = 0;
let targetScaleY = 0;
let element: HTMLElement | null = this.root;

while (element !== null) {
const isBodyElement = element === body;
if (isBodyElement) {
targetTop = 0;
targetBottom = defaultView.innerHeight;
const targetRect = element.getBoundingClientRect();
targetScaleY = targetRect.height / element.offsetHeight;
} else {
const targetRect = element.getBoundingClientRect();
targetTop = targetRect.top;
targetBottom = targetRect.bottom;
targetScaleY = targetRect.height / element.offsetHeight;
}
let diff = 0;

const style = getComputedStyle(element);
const borderTop = parseInt(style.borderTopWidth as string, 10);
const borderBottom = parseInt(style.borderBottomWidth as string, 10);

if (top < targetTop) {
diff = top - targetTop;
} else if (bottom > targetBottom) {
const scrollbarHeight =
element.offsetHeight -
element.clientHeight -
borderTop -
borderBottom;

diff =
(bottom - targetBottom) / targetScaleY +
scrollbarHeight +
borderBottom;
}

if (diff) {
if (isBodyElement) {
defaultView.scrollBy(0, diff);
} else {
const scrollTop = element.scrollTop;
element.scrollTop += diff;
const yOffset = (element.scrollTop - scrollTop) * targetScaleY;
top -= yOffset;
bottom -= yOffset;
}
}
if (isBodyElement) {
break;
}
element = element.parentElement;
}
// Virtual element is not supported so we have to use a workaround.
// https://github.com/scroll-into-view/compute-scroll-into-view
const virtualElement = {
nodeType: 1,
parentElement: this.root,
getBoundingClientRect() {
return new DOMRect(
bounds.left,
bounds.top,
bounds.width,
bounds.height,
);
},
} as unknown as Element;

compute(virtualElement, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
}).forEach(({ el, top, left }) => {
el.scrollTop = top;
el.scrollLeft = left;
});
}

setContents(
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"website"
],
"dependencies": {
"compute-scroll-into-view": "3.0.3",
"eventemitter3": "^4.0.7",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0",
Expand Down
128 changes: 94 additions & 34 deletions test/unit/core/quill.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,49 +999,109 @@ describe('Quill', () => {
});

describe('scrollSelectionIntoView', () => {
const getAncestorScrollTops = (element: HTMLElement) => {
const scrollTops: number[] = [];
let current = element.parentElement;
while (current) {
scrollTops.push(current.scrollTop);
current = current.parentElement;
}
return scrollTops;
const viewportRatio = (element: Element): Promise<number> => {
return new Promise(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
});
};

test('scrolls multiple ancestors', () => {
document.body.style.height = '500px';
const container = document.body.appendChild(
document.createElement('div'),
);
describe('scroll upward', () => {
test('scrolls multiple ancestors', async () => {
document.body.style.height = '500px';
const container = document.body.appendChild(
document.createElement('div'),
);

Object.assign(container.style, {
height: '100px',
overflow: 'scroll',
});
Object.assign(container.style, {
height: '100px',
overflow: 'scroll',
});

const space = container.appendChild(document.createElement('div'));
space.style.height = '80px';
const editorContainer = container.appendChild(
document.createElement('div'),
);
Object.assign(editorContainer.style, {
height: '100px',
overflow: 'scroll',
});

const editorContainer = container.appendChild(
document.createElement('div'),
);
Object.assign(editorContainer.style, {
height: '100px',
border: '10px solid red',
overflow: 'scroll',
const space = container.appendChild(document.createElement('div'));
space.style.height = '800px';

const quill = new Quill(editorContainer);

let text = '';
for (let i = 1; i < 200; i += 1) {
text += `line ${i}\n`;
}
quill.setContents(new Delta().insert(text));
quill.setSelection({ index: text.indexOf('line 10'), length: 4 });

container.scrollTop = -500;

expect(
await viewportRatio(
editorContainer.querySelector('p:nth-child(10)') as HTMLElement,
),
).toEqual(1);
expect(
await viewportRatio(
editorContainer.querySelector('p:nth-child(11)') as HTMLElement,
),
).toEqual(0);
});
});

describe('scroll downward', () => {
test('scrolls multiple ancestors', async () => {
document.body.style.height = '500px';
const container = document.body.appendChild(
document.createElement('div'),
);

const quill = new Quill(editorContainer);
Object.assign(container.style, {
height: '100px',
overflow: 'scroll',
});

let text = '';
for (let i = 0; i < 200; i += 1) {
text += `line ${i}\n`;
}
quill.setContents(new Delta().insert(text));
quill.setSelection({ index: text.indexOf('line 100'), length: 4 });
const space = container.appendChild(document.createElement('div'));
space.style.height = '80px';

expect(getAncestorScrollTops(editorContainer)).toEqual([90, 0, 0]);
const editorContainer = container.appendChild(
document.createElement('div'),
);
Object.assign(editorContainer.style, {
height: '100px',
overflow: 'scroll',
});

const quill = new Quill(editorContainer);

let text = '';
for (let i = 1; i < 200; i += 1) {
text += `line ${i}\n`;
}
quill.setContents(new Delta().insert(text));
quill.setSelection({ index: text.indexOf('line 100'), length: 4 });

expect(
await viewportRatio(
editorContainer.querySelector('p:nth-child(100)') as HTMLElement,
),
).toEqual(1);
expect(
await viewportRatio(
editorContainer.querySelector('p:nth-child(101)') as HTMLElement,
),
).toEqual(0);
});
});
});
});
2 changes: 1 addition & 1 deletion themes/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ class BaseTooltip extends Tooltip {
}

restoreFocus() {
this.quill.focus();
this.quill.focus({ preventScroll: true });
}

save() {
Expand Down

0 comments on commit a899b23

Please sign in to comment.