From b49c6084fa7afa47c526ed44da029ecef6a0d4d3 Mon Sep 17 00:00:00 2001 From: azu Date: Sun, 2 Aug 2020 17:02:32 +0900 Subject: [PATCH] fix(textchecker-element): when scroll and resize, update annotations Also, fix to show correct positon on scrollview Also annotationBox scroll with target textarea. fix https://github.com/textlint/editor/issues/7 --- .../src/attach-to-text-area.ts | 167 +++++++++--------- .../src/text-checker-element.ts | 66 +++++-- .../src/text-checker-store.ts | 8 + .../webextension/app/scripts/background.ts | 1 - 4 files changed, 140 insertions(+), 102 deletions(-) diff --git a/packages/textchecker-element/src/attach-to-text-area.ts b/packages/textchecker-element/src/attach-to-text-area.ts index 4976c07..f418ce3 100644 --- a/packages/textchecker-element/src/attach-to-text-area.ts +++ b/packages/textchecker-element/src/attach-to-text-area.ts @@ -58,92 +58,86 @@ export const attachToTextArea = ({ textAreaElement, lintingDebounceMs, lintEngin document.body.append(textCheckerPopup); const compositionHandler = createCompositionHandler(); - const update = pDebounce( - async () => { - // stop lint on IME composition - if (compositionHandler.onComposition) { - return; + const update = pDebounce(async () => { + // stop lint on IME composition + if (compositionHandler.onComposition) { + return; + } + const text = textAreaElement.value; + const results = await lintEngine.lintText({ + text + }); + const updateText = async (newText: string, card: TextCheckerCard) => { + const currentText = textAreaElement.value; + if (currentText === text && currentText !== newText) { + textAreaElement.value = newText; + await update(); + textCheckerPopup.dismissCard(card); } - const text = textAreaElement.value; - const results = await lintEngine.lintText({ - text - }); - const updateText = async (newText: string, card: TextCheckerCard) => { - const currentText = textAreaElement.value; - if (currentText === text && currentText !== newText) { - textAreaElement.value = newText; - await update(); - textCheckerPopup.dismissCard(card); - } - }; - const annotations = results.flatMap((result) => { - return result.messages.map((message) => { - const card: TextCheckerCard = { - id: message.ruleId + "::" + message.index, - message: message.message, - messageRuleId: message.ruleId, - fixable: Boolean(message.fix) - }; - return { - start: message.index, - end: message.index + 1, - onMouseEnter: ({ rectItem }: { rectItem: TextCheckerElementRectItem }) => { - textCheckerPopup.updateCard({ - card: card, - rect: { - top: - rectItem.boxBorderWidth + - rectItem.boxMarginTop + - rectItem.boxPaddingTop + - rectItem.boxAbsoluteY + - rectItem.top + - rectItem.height, - left: rectItem.boxAbsoluteX + rectItem.left, - width: rectItem.width + }; + const annotations = results.flatMap((result) => { + return result.messages.map((message) => { + const card: TextCheckerCard = { + id: message.ruleId + "::" + message.index, + message: message.message, + messageRuleId: message.ruleId, + fixable: Boolean(message.fix) + }; + return { + start: message.index, + end: message.index + 1, + onMouseEnter: ({ rectItem }: { rectItem: TextCheckerElementRectItem }) => { + textCheckerPopup.updateCard({ + card: card, + rect: { + top: + rectItem.boxBorderWidth + + rectItem.boxMarginTop + + rectItem.boxPaddingTop + + rectItem.boxAbsoluteY + + rectItem.top + + rectItem.height, + left: rectItem.boxAbsoluteX + rectItem.left, + width: rectItem.width + }, + handlers: { + async onFixText() { + const fixResults = await lintEngine.fixText({ + text, + message + }); + await updateText(fixResults.output, card); + }, + async onFixAll() { + const fixResults = await lintEngine.fixAll({ + text + }); + await updateText(fixResults.output, card); }, - handlers: { - async onFixText() { - const fixResults = await lintEngine.fixText({ - text, - message - }); - await updateText(fixResults.output, card); - }, - async onFixAll() { - const fixResults = await lintEngine.fixAll({ - text - }); - await updateText(fixResults.output, card); - }, - async onFixRule() { - const fixResults = await lintEngine.fixRule({ - text, - message - }); - await updateText(fixResults.output, card); - }, - onIgnore() { - console.log("onIgnore"); - }, - onSeeDocument() { - console.log("onSeeDocument"); - } + async onFixRule() { + const fixResults = await lintEngine.fixRule({ + text, + message + }); + await updateText(fixResults.output, card); + }, + onIgnore() { + console.log("onIgnore"); + }, + onSeeDocument() { + console.log("onSeeDocument"); } - }); - }, - onMouseLeave() { - textCheckerPopup.dismissCard(card); - } - }; - }); + } + }); + }, + onMouseLeave() { + textCheckerPopup.dismissCard(card); + } + }; }); - textChecker.updateAnnotations(annotations); - }, - lintingDebounceMs, - { - leading: true - } - ); + }); + textChecker.updateAnnotations(annotations); + }, lintingDebounceMs); // add event handlers textAreaElement.addEventListener("compositionstart", compositionHandler); textAreaElement.addEventListener("compositionend", compositionHandler); @@ -152,18 +146,21 @@ export const attachToTextArea = ({ textAreaElement, lintingDebounceMs, lintEngin // when resize element, update annotation // @ts-expect-error const resizeObserver = new ResizeObserver(() => { + textChecker.resetAnnotations(); console.log("textarea resize"); update(); }); resizeObserver.observe(textAreaElement); // when scroll window, update annotation - window.addEventListener("scroll", function () { + window.addEventListener("scroll", () => { console.log("window scroll"); + textChecker.resetAnnotations(); update(); }); - // when scroll the element, update annoation - textAreaElement.addEventListener("scroll", function () { + // when scroll the element, update annotation + textAreaElement.addEventListener("scroll", () => { console.log("textarea scroll"); + textChecker.resetAnnotations(); update(); }); }; diff --git a/packages/textchecker-element/src/text-checker-element.ts b/packages/textchecker-element/src/text-checker-element.ts index 941273e..a6e3154 100644 --- a/packages/textchecker-element/src/text-checker-element.ts +++ b/packages/textchecker-element/src/text-checker-element.ts @@ -45,24 +45,44 @@ export class TextCheckerElement extends HTMLElement { } const shadow = this.attachShadow({ mode: "open" }); const overlay = document.createElement("div"); + overlay.className = "overlay"; overlay.setAttribute( "style", "color: transparent; border: 1px dotted blue; position: absolute; top: 0px; left: 0px; pointer-events: none;" ); const annotationBox = document.createElement("div"); + annotationBox.className = "annotationBox"; overlay.append(annotationBox); shadow.append(overlay); this.annotationBox = annotationBox; this.targetElement.addEventListener("mousemove", this.onMouseUpdate); + // when scroll the element, update annoation + this.targetElement.addEventListener("scroll", this.updateOnScroll); this.store.onChange(() => { this.renderAnnotationMarkers(this.store.get()); }); } + updateOnScroll = () => {}; + + disconnectedCallback() { + this.targetElement.removeEventListener("mousemove", this.onMouseUpdate); + this.targetElement.removeEventListener("scroll", this.updateOnScroll); + } + + resetAnnotations() { + if (this.store.get().rectItems.length === 0) { + return; // no update + } + this.store.update({ + rectItems: [] + }); + } + updateAnnotations(annotationItems: AnnotationItem[]) { const target = this.targetElement; const targetStyle = window.getComputedStyle(target); - const copyAttributes = ["box-sizing", "overflow"] as const; + const copyAttributes = ["box-sizing"] as const; const copyStyle = copyAttributes .map((attr) => { return `${attr}: ${targetStyle.getPropertyValue(attr)};`; @@ -70,16 +90,18 @@ export class TextCheckerElement extends HTMLElement { .join(""); this.annotationBox.setAttribute( "style", - `color: transparent; position: absolute; pointer-events: none; ${copyStyle}` + `color: transparent; overflow:hidden; position: absolute; pointer-events: none; ${copyStyle}` ); // Ref: https://github.com/yuku/textoverlay + // Outer position // update annotation box that align with target textarea // top-left (0,0) // read styles form target element - const top = target.offsetTop; - const left = target.offsetLeft; - const height = target.offsetHeight; - const width = + const offsetTop = target.offsetTop; + const offsetLeft = target.offsetLeft; + const offsetHeight = target.offsetHeight; + console.log({ offsetTop, offsetLeft, offsetHeight }); + const offsetWidth = target.clientWidth + parseInt(targetStyle.borderLeftWidth || "0", 10) + parseInt(targetStyle.borderRightWidth || "0", 10); @@ -87,10 +109,10 @@ export class TextCheckerElement extends HTMLElement { const textareaZIndex = targetStyle.zIndex !== null && targetStyle.zIndex !== "auto" ? +targetStyle.zIndex : 0; // updates style this.annotationBox.style.zIndex = `${textareaZIndex + 1}`; - this.annotationBox.style.left = `${left}px`; - this.annotationBox.style.top = `${top}px`; - this.annotationBox.style.height = `${height}px`; - this.annotationBox.style.width = `${width}px`; + this.annotationBox.style.left = `${offsetLeft}px`; + this.annotationBox.style.top = `${offsetTop}px`; + this.annotationBox.style.height = `${offsetHeight}px`; + this.annotationBox.style.width = `${offsetWidth}px`; // box const fontSize: number = toPX(targetStyle.getPropertyValue("font-size")) ?? 16.123; const boxMarginTop: number = toPX(targetStyle.getPropertyValue("margin-top")) ?? 0; @@ -103,6 +125,16 @@ export class TextCheckerElement extends HTMLElement { const boxAbsoluteY: number = boundingClientRect.y; const boxWidth: number = boundingClientRect.width; const boxHeight: number = boundingClientRect.height; + // Inner position + // textarea is scrollable element + const visibleArea = { + top: target.scrollTop, + left: target.scrollLeft, + width: boxWidth, + height: boxHeight + }; + this.annotationBox.scrollTop = target.scrollTop; + this.annotationBox.scrollLeft = target.scrollLeft; const rectItems = annotationItems.flatMap((annotation, index) => { const start = annotation.start; const end = annotation.end; @@ -124,8 +156,10 @@ export class TextCheckerElement extends HTMLElement { ? [ { index, - left: startCoordinate.left, - top: startCoordinate.top, + // left and top is visible position + // annotationBox(textarea) also scroll with same position of actual textarea + left: startCoordinate.left - visibleArea.left, + top: startCoordinate.top - visibleArea.top, height: fontSize, //startCoordinate.height, width: endCoordinate.left - startCoordinate.left, boxMarginTop, @@ -143,8 +177,8 @@ export class TextCheckerElement extends HTMLElement { [ { index, - left: startCoordinate.left, - top: startCoordinate.top, + left: startCoordinate.left - visibleArea.left, + top: startCoordinate.top - visibleArea.top, height: fontSize, //startCoordinate.height, width: (startCoordinate?._div?.getBoundingClientRect()?.width ?? 0) - startCoordinate.left, @@ -160,8 +194,8 @@ export class TextCheckerElement extends HTMLElement { }, { index, - left: 0, - top: endCoordinate.top, + left: -visibleArea.left, + top: endCoordinate.top - visibleArea.top, height: fontSize, width: (startCoordinate?._div?.getBoundingClientRect()?.left ?? 0) + endCoordinate.left, boxMarginTop, diff --git a/packages/textchecker-element/src/text-checker-store.ts b/packages/textchecker-element/src/text-checker-store.ts index 49bb733..1f90df9 100644 --- a/packages/textchecker-element/src/text-checker-store.ts +++ b/packages/textchecker-element/src/text-checker-store.ts @@ -27,6 +27,10 @@ export type TextCheckerElementRectItem = { boxHeight: number; }; export type TextCheckerState = { + visibleTop: number; + visibleLeft: number; + visibleWidth: number; + visibleHeight: number; rectItems: TextCheckerElementRectItem[]; annotationItems: AnnotationItem[]; mouseHoverReactIdMap: Map; @@ -34,6 +38,10 @@ export type TextCheckerState = { }; export const createTextCheckerStore = (initialState?: Partial) => { let textCheckerState: TextCheckerState = { + visibleTop: 0, + visibleLeft: 0, + visibleWidth: 0, + visibleHeight: 0, rectItems: [], annotationItems: [], highlightRectIdSet: new Set(), diff --git a/packages/webextension/app/scripts/background.ts b/packages/webextension/app/scripts/background.ts index cab2f36..f9e1a23 100644 --- a/packages/webextension/app/scripts/background.ts +++ b/packages/webextension/app/scripts/background.ts @@ -123,7 +123,6 @@ browser.runtime.onConnect.addListener(async (port) => { .fixRule({ text: output, message }) .then((result) => { output = result.output; - console.log(output, result); return result; }); });