Skip to content

Commit

Permalink
fix(textchecker-element): when scroll and resize, update annotations
Browse files Browse the repository at this point in the history
Also, fix to show correct positon on scrollview

Also annotationBox scroll with target textarea.
fix #7
  • Loading branch information
azu committed Aug 2, 2020
1 parent 291ac1d commit b49c608
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 102 deletions.
167 changes: 82 additions & 85 deletions packages/textchecker-element/src/attach-to-text-area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
});
};
66 changes: 50 additions & 16 deletions packages/textchecker-element/src/text-checker-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,52 +45,74 @@ 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)};`;
})
.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);
// const textareaScrollTop = target.scrollTop;
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;
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions packages/textchecker-element/src/text-checker-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,21 @@ export type TextCheckerElementRectItem = {
boxHeight: number;
};
export type TextCheckerState = {
visibleTop: number;
visibleLeft: number;
visibleWidth: number;
visibleHeight: number;
rectItems: TextCheckerElementRectItem[];
annotationItems: AnnotationItem[];
mouseHoverReactIdMap: Map<TextCheckerElementRectItem["index"], boolean>;
highlightRectIdSet: Set<TextCheckerElementRectItem["index"]>;
};
export const createTextCheckerStore = (initialState?: Partial<TextCheckerState>) => {
let textCheckerState: TextCheckerState = {
visibleTop: 0,
visibleLeft: 0,
visibleWidth: 0,
visibleHeight: 0,
rectItems: [],
annotationItems: [],
highlightRectIdSet: new Set(),
Expand Down
1 change: 0 additions & 1 deletion packages/webextension/app/scripts/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});
Expand Down

0 comments on commit b49c608

Please sign in to comment.