textchecker-element
Version:
Overlay text checker web compoentns.
308 lines • 13.8 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.attachToTextArea = void 0;
const text_checker_element_1 = require("./text-checker-element");
const text_checker_popup_element_1 = require("./text-checker-popup-element");
const p_debounce_1 = __importDefault(require("p-debounce"));
const logger_1 = require("./util/logger");
const createCompositionHandler = () => {
let onComposition = false;
return {
onComposition,
handleEvent: (event) => {
if (event.type === "compositionend") {
onComposition = false;
}
else if (event.type === "compositionstart") {
onComposition = true;
}
}
};
};
const createTextCheckerPopupElement = (args) => {
const textCheckerPopup = new text_checker_popup_element_1.TextCheckerPopupElement(args);
document.body.append(textCheckerPopup);
return textCheckerPopup;
};
/**
* Return true if the element in viewport
* @param element
*/
function isVisibleInViewport(element) {
const style = window.getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden") {
return false;
}
const rect = element.getBoundingClientRect();
if (rect.height === 0 || rect.width === 0) {
return false;
}
return (rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth));
}
/**
* Dismiss all popup
*
* - Update text(includes fixed)
* - Scroll textarea/Scroll window
* - Focus on textarea
* - Click out of textarea/popup
* - popup → textarea → other → dismiss
* - textarea → popup → other → dismiss
*/
/**
* Dismiss a single popup(500ms delay)
*
* - Leave from popup
* - Leave from RectItem
* - Focus on textarea
*
*/
/**
* Show popup condition
* - onUpdate
*/
/**
* Attach text-checker component to `<textarea>` element
*/
const attachToTextArea = ({ textAreaElement, lintingDebounceMs, lintEngine }) => {
if (!textAreaElement) {
(0, logger_1.debug)("Can not attach. No textarea", textAreaElement);
return () => { };
}
if (textAreaElement.readOnly) {
(0, logger_1.debug)("Can not attach textarea that is readonly", textAreaElement);
return () => { };
}
if (textAreaElement.dataset.attachedTextCheckerElement === "true") {
(0, logger_1.debug)("Can not attach textarea that is already attached", textAreaElement);
return () => { };
}
const dismissCards = () => {
(0, logger_1.debug)("dismissCards", {
textCheckerPopup: textCheckerPopup.isHovering,
textChecker: textChecker.isHovering,
textCheckerF: textChecker.isFocus
});
if (!textCheckerPopup.isHovering && !textChecker.isHovering && !textChecker.isFocus) {
textCheckerPopup.dismissCards();
textChecker.resetHoverState();
}
};
const textCheckerPopup = createTextCheckerPopupElement({
onLeave() {
dismissCards();
}
});
const textChecker = new text_checker_element_1.TextCheckerElement({
targetElement: textAreaElement,
hoverPadding: 20,
onLeave() {
dismissCards();
}
});
textAreaElement.before(textChecker);
const compositionHandler = createCompositionHandler();
const update = (0, p_debounce_1.default)(() => __awaiter(void 0, void 0, void 0, function* () {
if (!isVisibleInViewport(textAreaElement)) {
return;
}
// stop lint on IME composition
if (compositionHandler.onComposition) {
return;
}
// dismiss card before update annotations
// dismissCards();
const text = textAreaElement.value;
const results = yield lintEngine.lintText({
text
});
(0, logger_1.debug)("lint results", results);
const updateText = (newText, card) => __awaiter(void 0, void 0, void 0, function* () {
const currentText = textAreaElement.value;
if (currentText === text && currentText !== newText) {
textAreaElement.value = newText;
yield updateOrClearAnnotationsIfFailed();
textCheckerPopup.dismissCard(card);
}
});
const annotations = results.flatMap((result) => {
return result.messages.map((message) => {
var _a, _b, _c;
// message.range is introduced in textlint@12.2.0
// https://github.com/textlint/textlint/releases/tag/v12.2.0
const range = (_c = (_a = message.range) !== null && _a !== void 0 ? _a : (_b = message.fix) === null || _b === void 0 ? void 0 : _b.range) !== null && _c !== void 0 ? _c : [message.index, message.index + 1];
const card = {
id: `${message.ruleId}::${range[0]}-${range[1]}`,
message: message.message,
messageRuleId: message.ruleId,
fixable: Boolean(message.fix)
};
let dismissTimerId = null;
return {
id: `${message.ruleId}::${range[0]}-${range[1]}`,
start: range[0],
end: range[1],
onMouseEnter: ({ rectItem }) => {
(0, logger_1.debug)("annotation - onMouseEnter");
if (dismissTimerId) {
clearTimeout(dismissTimerId);
}
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: {
onFixText() {
return __awaiter(this, void 0, void 0, function* () {
const fixResults = yield lintEngine.fixText({
text,
messages: [message]
});
yield updateText(fixResults.output, card);
});
},
onFixAll() {
return __awaiter(this, void 0, void 0, function* () {
const fixResults = yield lintEngine.fixText({
text,
messages: result.messages
});
yield updateText(fixResults.output, card);
});
},
onFixRule() {
return __awaiter(this, void 0, void 0, function* () {
const messages = result.messages.filter((aMessage) => aMessage.ruleId === message.ruleId);
const fixResults = yield lintEngine.fixText({
text,
messages
});
yield updateText(fixResults.output, card);
});
},
onIgnore() {
return __awaiter(this, void 0, void 0, function* () {
yield lintEngine.ignoreText({
text,
message
});
yield updateOrClearAnnotationsIfFailed();
});
},
onSeeDocument() {
const id = message.ruleId.includes("/")
? message.ruleId.split("/")[1]
: message.ruleId;
window.open(`https://github.com/search?q=textlint ${encodeURIComponent(id)}`, "_blank", "noopener");
}
}
});
},
onMouseLeave(_a) {
return __awaiter(this, arguments, void 0, function* ({ rectItem }) {
try {
(0, logger_1.debug)("annotation - onMouseLeave");
dismissTimerId = setTimeout(() => {
const isHover = textChecker.isHoverRectItem(rectItem);
(0, logger_1.debug)("dismiss", {
textCheckerPopup: textCheckerPopup.isHovering,
isRectElementHover: isHover
});
if (textCheckerPopup.isHovering || isHover) {
return;
}
textCheckerPopup.dismissCard(card);
}, 500);
}
catch (error) {
(0, logger_1.debug)("Abort dismiss popup", error);
}
});
}
};
});
});
(0, logger_1.debug)("annotations", annotations);
textChecker.updateAnnotations(annotations);
}), lintingDebounceMs);
const updateOrClearAnnotationsIfFailed = () => __awaiter(void 0, void 0, void 0, function* () {
try {
yield update();
}
catch (error) {
(0, logger_1.debug)("update error", error);
textChecker.updateAnnotations([]);
}
});
// Events
// when resize element, update annotation
const resizeObserver = new ResizeObserver(() => {
(0, logger_1.debug)("ResizeObserver do update");
textCheckerPopup.dismissCards();
textChecker.resetAnnotations();
updateOrClearAnnotationsIfFailed();
});
resizeObserver.observe(textAreaElement);
// when scroll window, update annotation
const onScroll = () => {
textCheckerPopup.dismissCards();
textChecker.resetAnnotations();
updateOrClearAnnotationsIfFailed();
};
const onFocus = () => {
textCheckerPopup.dismissCards();
updateOrClearAnnotationsIfFailed();
};
const onBlur = (event) => {
// does not dismiss on click popup items(require tabindex)
if (event.relatedTarget === textChecker || event.relatedTarget === textCheckerPopup) {
return;
}
textCheckerPopup.dismissCards();
};
textAreaElement.addEventListener("compositionstart", compositionHandler);
textAreaElement.addEventListener("compositionend", compositionHandler);
textAreaElement.addEventListener("input", update);
textAreaElement.addEventListener("focus", onFocus);
textAreaElement.addEventListener("blur", onBlur);
textAreaElement.addEventListener("focusout", dismissCards);
window.addEventListener("scroll", onScroll);
// when scroll the element, update annotation
textAreaElement.addEventListener("scroll", onScroll);
updateOrClearAnnotationsIfFailed();
return () => {
window.removeEventListener("scroll", onScroll);
textAreaElement.removeEventListener("scroll", onScroll);
textAreaElement.removeEventListener("compositionstart", compositionHandler);
textAreaElement.removeEventListener("compositionend", compositionHandler);
textAreaElement.removeEventListener("input", update);
textAreaElement.removeEventListener("focus", onFocus);
textAreaElement.removeEventListener("blur", onBlur);
resizeObserver.disconnect();
};
};
exports.attachToTextArea = attachToTextArea;
//# sourceMappingURL=attach-to-text-area.js.map