poanse_obsidian-taskmap/src/legacy/text-input.js
2025-12-17 04:51:25 +03:00

720 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Container, Graphics, Text, TextStyle } from "pixi.js";
export default class TextInput extends Container {
constructor(mountEl, styles) {
super();
this.mountEl = mountEl;
this._input_style = Object.assign(
{
position: "absolute",
background: "none",
border: "none",
outline: "none",
transformOrigin: "0 0",
lineHeight: "1",
},
styles.input,
);
if (styles.box)
this._box_generator =
typeof styles.box === "function"
? styles.box
: new DefaultBoxGenerator(styles.box);
else this._box_generator = null;
if (this._input_style.hasOwnProperty("multiline")) {
this._multiline = !!this._input_style.multiline;
delete this._input_style.multiline;
} else this._multiline = false;
this._box_cache = {};
this._previous = {};
this._dom_added = false;
this._dom_visible = true;
// Ставим поейсхолдер, но он не использутся если непуст dominput. А он заполняется парой строчек ниже
this._placeholder = styles["text"];
this._placeholderColor = styles["input"]["color"];
this._selection = [0, 0];
this._restrict_value = "";
this._createDOMInput();
this._dom_input.value = styles["text"];
this.substituteText = true;
this._setState("DEFAULT");
this._addListeners();
//
// this.worldAlpha = 1;
this.worldVisible = true;
}
// GETTERS & SETTERS
get substituteText() {
return this._substituted;
}
set substituteText(substitute) {
if (this._substituted == substitute) return;
this._substituted = substitute;
if (substitute) {
this._createSurrogate();
this._dom_visible = false;
} else {
this._destroySurrogate();
this._dom_visible = true;
}
this.placeholder = this._placeholder;
this._update();
}
get placeholder() {
return this._placeholder;
}
set placeholder(text) {
this._placeholder = text;
if (this._substituted) {
this._updateSurrogate();
this._dom_input.placeholder = "";
} else {
this._dom_input.placeholder = text;
}
}
get disabled() {
return this._disabled;
}
set disabled(disabled) {
this._disabled = disabled;
this._dom_input.disabled = disabled;
this._setState(disabled ? "DISABLED" : "DEFAULT");
}
get maxLength() {
return this._max_length;
}
set maxLength(length) {
this._max_length = length;
this._dom_input.setAttribute("maxlength", length);
}
get restrict() {
return this._restrict_regex;
}
set restrict(regex) {
if (regex instanceof RegExp) {
regex = regex.toString().slice(1, -1);
if (regex.charAt(0) !== "^") regex = "^" + regex;
if (regex.charAt(regex.length - 1) !== "$") regex = regex + "$";
regex = new RegExp(regex);
} else {
regex = new RegExp("^[" + regex + "]*$");
}
this._restrict_regex = regex;
}
get text() {
return this._dom_input.value;
}
set text(text) {
this._dom_input.value = text;
if (this._substituted) this._updateSurrogate();
}
get htmlInput() {
return this._dom_input;
}
focus() {
console.log("focus called");
if (this._substituted && !this.dom_visible) {
console.log("dom input visible: true");
this._setDOMInputVisible(true);
}
this._dom_input.focus();
}
blur() {
this._dom_input.blur();
}
select() {
this.focus();
this._dom_input.select();
}
setInputStyle(key, value) {
this._input_style[key] = value;
this._dom_input.style[key] = value;
if (this._substituted && (key === "fontFamily" || key === "fontSize"))
this._updateFontMetrics();
if (this._last_renderer) this._update();
}
destroy(options) {
this._destroyBoxCache();
super.destroy(options);
}
// SETUP
_createDOMInput() {
if (this._multiline) {
this._dom_input = document.createElement("textarea");
this._dom_input.style.resize = "none";
} else {
this._dom_input = document.createElement("input");
this._dom_input.type = "text";
}
for (let key in this._input_style) {
this._dom_input.style[key] = this._input_style[key];
}
}
_addListeners() {
this.on("added", this._onAdded.bind(this));
this.on("removed", this._onRemoved.bind(this));
this._dom_input.addEventListener(
"keydown",
this._onInputKeyDown.bind(this),
);
this._dom_input.addEventListener(
"input",
this._onInputInput.bind(this),
);
this._dom_input.addEventListener(
"keyup",
this._onInputKeyUp.bind(this),
);
this._dom_input.addEventListener("focus", this._onFocused.bind(this));
this._dom_input.addEventListener("blur", this._onBlurred.bind(this));
}
_onInputKeyDown(e) {
this._selection = [
this._dom_input.selectionStart,
this._dom_input.selectionEnd,
];
this.emit("keydown", e.keyCode);
}
_onInputInput(e) {
if (this._restrict_regex) this._applyRestriction();
if (this._substituted) this._updateSubstitution();
this.emit("input", this.text);
}
_onInputKeyUp(e) {
this.emit("keyup", e.keyCode);
}
_onFocused() {
this._setState("FOCUSED");
this.emit("focus");
}
_onBlurred() {
this._setState("DEFAULT");
this.emit("blur");
}
_onAdded() {
this.mountEl.appendChild(this._dom_input);
// document.body.appendChild(this._dom_input);
this._dom_input.style.display = "none";
this._dom_added = true;
}
_onRemoved() {
this.mountEl.removeChild(this._dom_input);
// document.body.removeChild(this._dom_input);
this._dom_added = false;
}
_setState(state) {
this.state = state;
this._updateBox();
if (this._substituted) this._updateSubstitution();
}
// RENDER & UPDATE
render(renderer) {
// super.render(renderer);
this._resolution = renderer.resolution;
this._last_renderer = renderer;
this._canvas_bounds = this._getCanvasBounds();
if (this._needsUpdate()) this._update();
}
_update() {
this._updateDOMInput();
if (this._substituted) this._updateSurrogate();
this._updateBox();
}
_updateBox() {
if (!this._box_generator) return;
if (this._needsNewBoxCache()) this._buildBoxCache();
if (
this.state == this._previous.state &&
this._box == this._box_cache[this.state]
)
return;
if (this._box) this.removeChild(this._box);
this._box = this._box_cache[this.state];
this.addChildAt(this._box, 0);
this._previous.state = this.state;
}
_updateSubstitution() {
if (this.state === "FOCUSED") {
this._dom_visible = true;
this._surrogate.visible = this.text.length === 0;
} else {
this._dom_visible = false;
this._surrogate.visible = true;
}
this._updateDOMInput();
this._updateSurrogate();
}
_updateDOMInput() {
if (!this._canvas_bounds) {
console.log("No Canvas Bounds");
return;
}
this._dom_input.style.top = (this._canvas_bounds.top || 0) + "px";
this._dom_input.style.left = (this._canvas_bounds.left || 0) + "px";
var transform = this._getDOMRelativeWorldTransform();
// !!!!
// transform.tx += 10;
// transform.ty += 50;
this._dom_input.style.transform = this._pixiMatrixToCSS(transform);
console.log(
JSON.stringify({
top: this._dom_input.style.top,
left: this._dom_input.style.left,
transform: this._dom_input.style.transform,
}),
);
this._dom_input.style.opacity = this.worldAlpha;
console.log("Alpha", this.worldAlpha);
this._setDOMInputVisible(this.worldVisible && this._dom_visible);
console.log("world visible", this.worldVisible);
this._previous.canvas_bounds = this._canvas_bounds;
this._previous.world_transform = this.worldTransform.clone();
this._previous.world_alpha = this.worldAlpha;
this._previous.world_visible = this.worldVisible;
}
_applyRestriction() {
if (this._restrict_regex.test(this.text)) {
this._restrict_value = this.text;
} else {
this.text = this._restrict_value;
this._dom_input.setSelectionRange(
this._selection[0],
this._selection[1],
);
}
}
// STATE COMPAIRSON (FOR PERFORMANCE BENEFITS)
_needsUpdate() {
return (
!this._comparePixiMatrices(
this.worldTransform,
this._previous.world_transform,
) ||
!this._compareClientRects(
this._canvas_bounds,
this._previous.canvas_bounds,
) ||
this.worldAlpha != this._previous.world_alpha ||
this.worldVisible != this._previous.world_visible
);
}
_needsNewBoxCache() {
let input_bounds = this._getDOMInputBounds();
return (
!this._previous.input_bounds ||
input_bounds.width != this._previous.input_bounds.width ||
input_bounds.height != this._previous.input_bounds.height
);
}
// INPUT SUBSTITUTION
_createSurrogate() {
this._surrogate_hitbox = new Graphics();
this._surrogate_hitbox.alpha = 0;
this._surrogate_hitbox.interactive = true;
this._surrogate_hitbox.cursor = "text";
this._surrogate_hitbox.on(
"pointerdown",
this._onSurrogateFocus.bind(this),
);
this.addChild(this._surrogate_hitbox);
this._surrogate_mask = new Graphics();
this.addChild(this._surrogate_mask);
this._surrogate = new Text({
text: "",
resolution: 8,
});
this.addChild(this._surrogate);
this._surrogate.mask = this._surrogate_mask;
this._updateFontMetrics();
this._updateSurrogate();
}
_updateSurrogate() {
let padding = this._deriveSurrogatePadding();
let input_bounds = this._getDOMInputBounds();
this._surrogate.style = this._deriveSurrogateStyle();
this._surrogate.style.padding = Math.max.apply(Math, padding);
this._surrogate.y = this._multiline
? padding[0]
: (input_bounds.height - this._surrogate.height) / 2;
this._surrogate.x = padding[3];
this._surrogate.text = this._deriveSurrogateText();
switch (this._surrogate.style.align) {
case "left":
this._surrogate.x = padding[3];
break;
case "center":
this._surrogate.x =
input_bounds.width * 0.5 - this._surrogate.width * 0.5;
break;
case "right":
this._surrogate.x =
input_bounds.width - padding[1] - this._surrogate.width;
break;
}
this._updateSurrogateHitbox(input_bounds);
this._updateSurrogateMask(input_bounds, padding);
}
_updateSurrogateHitbox(bounds) {
this._surrogate_hitbox.clear();
this._surrogate_hitbox.beginFill(0);
this._surrogate_hitbox.drawRect(0, 0, bounds.width, bounds.height);
this._surrogate_hitbox.endFill();
this._surrogate_hitbox.interactive = !this._disabled;
}
_updateSurrogateMask(bounds, padding) {
this._surrogate_mask.clear();
this._surrogate_mask.beginFill(0);
this._surrogate_mask.drawRect(
padding[3],
0,
bounds.width - padding[3] - padding[1],
bounds.height,
);
this._surrogate_mask.endFill();
}
_destroySurrogate() {
if (!this._surrogate) return;
this.removeChild(this._surrogate);
this.removeChild(this._surrogate_mask);
this.removeChild(this._surrogate_hitbox);
this._surrogate.destroy();
this._surrogate_hitbox.destroy();
this._surrogate = null;
this._surrogate_hitbox = null;
}
_onSurrogateFocus() {
this._setDOMInputVisible(true);
//sometimes the input is not being focused by the mouseclick
setTimeout(this._ensureFocus.bind(this), 10);
}
_ensureFocus() {
if (!this._hasFocus()) this.focus();
}
_deriveSurrogateStyle() {
let style = new TextStyle();
for (var key in this._input_style) {
switch (key) {
case "color":
style.fill = this._input_style.color;
break;
case "fontFamily":
case "fontSize":
case "fontWeight":
case "fontVariant":
case "fontStyle":
style[key] = this._input_style[key];
break;
case "letterSpacing":
style.letterSpacing = parseFloat(
this._input_style.letterSpacing,
);
break;
case "textAlign":
style.align = this._input_style.textAlign;
break;
}
}
if (this._multiline) {
style.lineHeight = parseFloat(style.fontSize);
style.wordWrap = true;
style.wordWrapWidth = this._getDOMInputBounds().width;
}
if (this._dom_input.value.length === 0)
style.fill = this._placeholderColor;
return style;
}
_deriveSurrogatePadding() {
let indent = this._input_style.textIndent
? parseFloat(this._input_style.textIndent)
: 0;
if (this._input_style.padding && this._input_style.padding.length > 0) {
let components = this._input_style.padding.trim().split(" ");
if (components.length == 1) {
let padding = parseFloat(components[0]);
return [padding, padding, padding, padding + indent];
} else if (components.length == 2) {
let paddingV = parseFloat(components[0]);
let paddingH = parseFloat(components[1]);
return [paddingV, paddingH, paddingV, paddingH + indent];
} else if (components.length == 4) {
let padding = components.map((component) => {
return parseFloat(component);
});
padding[3] += indent;
return padding;
}
}
return [0, 0, 0, indent];
}
_deriveSurrogateText() {
if (this._dom_input.value.length === 0) return this._placeholder;
if (this._dom_input.type == "password")
return "•".repeat(this._dom_input.value.length);
return this._dom_input.value;
}
_updateFontMetrics() {
const style = this._deriveSurrogateStyle();
// const font = style.toFontString()
// this._font_metrics = TextMetrics.measureFont(font)
}
// CACHING OF INPUT BOX GRAPHICS
_buildBoxCache() {
this._destroyBoxCache();
let states = ["DEFAULT", "FOCUSED", "DISABLED"];
let input_bounds = this._getDOMInputBounds();
for (let state of states) {
this._box_cache[state] = this._box_generator(
input_bounds.width,
input_bounds.height,
state,
);
}
this._previous.input_bounds = input_bounds;
}
_destroyBoxCache() {
if (this._box) {
this.removeChild(this._box);
this._box = null;
}
for (let i in this._box_cache) {
this._box_cache[i].destroy();
this._box_cache[i] = null;
delete this._box_cache[i];
}
}
// HELPER FUNCTIONS
_hasFocus() {
return document.activeElement === this._dom_input;
}
_setDOMInputVisible(visible) {
this._dom_input.style.display = visible ? "block" : "none";
}
_getCanvasBounds() {
console.log("getting bounds");
// let rect = this._last_renderer.view.getBoundingClientRect();
let rect = this._last_renderer.view.screen;
console.log(`Bounds: ${JSON.stringify(rect)}`);
let bounds = {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
};
bounds.left += window.scrollX;
bounds.top += window.scrollY;
return bounds;
}
_getDOMInputBounds() {
let remove_after = false;
if (!this._dom_added) {
// document.body.appendChild(this._dom_input);
this.mountEl.appendChild(this._dom_input);
remove_after = true;
}
let org_transform = this._dom_input.style.transform;
let org_display = this._dom_input.style.display;
this._dom_input.style.transform = "";
this._dom_input.style.display = "block";
let bounds = this._dom_input.getBoundingClientRect();
this._dom_input.style.transform = org_transform;
this._dom_input.style.display = org_display;
if (remove_after) {
this.mountEl.removeChild(this._dom_input);
// document.body.removeChild(this._dom_input);
}
return bounds;
}
_getDOMRelativeWorldTransform() {
// let canvas_bounds = this._last_renderer.view.getBoundingClientRect();
let canvas_bounds = this._last_renderer.view.screen;
let matrix = this.worldTransform.clone();
console.log(JSON.stringify(matrix));
matrix.scale(this._resolution, this._resolution);
matrix.scale(
canvas_bounds.width / this._last_renderer.width,
canvas_bounds.height / this._last_renderer.height,
);
return matrix;
}
_pixiMatrixToCSS(m) {
return "matrix(" + [m.a, m.b, m.c, m.d, m.tx, m.ty].join(",") + ")";
}
_comparePixiMatrices(m1, m2) {
if (!m1 || !m2) return false;
return (
m1.a == m2.a &&
m1.b == m2.b &&
m1.c == m2.c &&
m1.d == m2.d &&
m1.tx == m2.tx &&
m1.ty == m2.ty
);
}
_compareClientRects(r1, r2) {
if (!r1 || !r2) return false;
return (
r1.left == r2.left &&
r1.top == r2.top &&
r1.width == r2.width &&
r1.height == r2.height
);
}
}
function DefaultBoxGenerator(styles) {
styles = styles || { fill: 0xcccccc };
if (styles.default) {
styles.focused = styles.focused || styles.default;
styles.disabled = styles.disabled || styles.default;
} else {
let temp_styles = styles;
styles = {};
styles.default = styles.focused = styles.disabled = temp_styles;
}
return function (w, h, state) {
let style = styles[state.toLowerCase()];
let box = new Graphics({
// roundPixels: true,
// resolution: 8,
});
if (style.fill) box.beginFill(style.fill);
if (style.stroke)
box.lineStyle(
style.stroke.width || 1,
style.stroke.color || 0,
style.stroke.alpha || 1,
);
if (style.rounded) box.drawRoundedRect(0, 0, w, h, style.rounded);
else box.drawRect(0, 0, w, h);
box.endFill();
box.closePath();
return box;
};
}