Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: ui5-textarea caret coordinates #7907

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/main/src/TextArea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,22 @@ class TextArea extends UI5Element implements IFormElement {
this.previousValue = this.getInputDomRef().value;
}

get selectionStart() {
return this.shadowRoot!.querySelector("textarea")!.selectionStart;
}

set selectionStart(sel) {
this.shadowRoot!.querySelector("textarea")!.selectionStart = sel;
}

get selectionEnd() {
return this.shadowRoot!.querySelector("textarea")!.selectionEnd;
}

set selectionEnd(sel) {
this.shadowRoot!.querySelector("textarea")!.selectionEnd = sel;
}

_onfocusout(e: FocusEvent) {
const eTarget = e.relatedTarget as HTMLElement;
const focusedOutToValueStateMessage = eTarget?.shadowRoot?.querySelector(".ui5-valuestatemessage-root");
Expand Down
209 changes: 209 additions & 0 deletions packages/main/test/pages/Selection-patched.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/html">

<head>
<meta charset="utf-8">

<title>TextArea</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">

<script data-ui5-config type="application/json">
{
"language": "EN"
}
</script>

<script>
// delete Document.prototype.adoptedStyleSheets
</script>


<script src="%VITE_BUNDLE_PATH%" type="module"></script>


<link rel="stylesheet" type="text/css" href="./styles/TextArea.css">
</head>

<body class="textarea1auto">


<section class="group">
<ui5-title>Simple TextArea</ui5-title>
<ui5-textarea id="basic-textarea" placeholder="Basic text area">
<div slot="valueStateMessage">
This msg will not be displayed as no value-state is set.
</div>
</ui5-textarea>

<ui5-list>
<ui5-li description="text in shadow root - test selection here">list item</ui5-li>
<ui5-li description="text in shadow root - test selection here">list item</ui5-li>
<ui5-li description="text in shadow root - test selection here">list item</ui5-li>
<ui5-li description="text in shadow root - test selection here">list item</ui5-li>
</ui5-list>

<div id="selection"></div>

</section>

<ui5-popover id="pop" opener="test" placement-type="Bottom">
<ui5-icon name="joule"></ui5-icon>
</ui5-popover>
<div id="test" style="user-select: none; width: 2px; height: 1.2em; background: red; position: absolute;"></div>
<div id="test2" style="user-select: none; width: 2px; height: 1.2em; background: red; position: absolute;"></div>
<script>
let internalSelectionEvents = 0;

function createCopy(textArea) {
var copy = document.createElement('div');
copy.textContent = textArea.value;
var style = getComputedStyle(textArea);
[
'fontFamily',
'fontSize',
'fontWeight',
'wordWrap',
'whiteSpace',
'borderLeftWidth',
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
].forEach(function(key) {
copy.style[key] = style[key];
});
const innerTextArea = textArea.shadowRoot.querySelector("textarea");
const innerTextAreaStyle = getComputedStyle(innerTextArea)
copy.style.whiteSpace = "pre";
copy.style.paddingTop = innerTextAreaStyle.paddingTop;
copy.style.paddingLeft = innerTextAreaStyle.paddingLeft;
copy.style.lineHeight = innerTextAreaStyle.lineHeight;
copy.style.overflow = 'auto';
copy.style.width = textArea.offsetWidth + 'px';
copy.style.height = textArea.offsetHeight + 'px';
copy.style.position = 'absolute';
copy.style.left = textArea.offsetLeft + 'px';
copy.style.top = textArea.offsetTop + 'px';
document.body.appendChild(copy);
return copy;
}

function getCaretPosition(textArea) {
var start = textArea.shadowRoot.querySelector("textarea").selectionStart;
var end = textArea.shadowRoot.querySelector("textarea").selectionEnd;
var direction = window.getSelect;
var copy = createCopy(textArea);
var range = document.createRange();
range.setStart(copy.firstChild, start);
range.setEnd(copy.firstChild, end);
var selection = document.getSelection();
selection.removeAllRanges();
selection.addRange(range);
var rect = range.getBoundingClientRect();
document.body.removeChild(copy);
// the following lines will trigger a select event that should be skipped
if (start !== end) {
internalSelectionEvents += 1;
}
textArea.shadowRoot.querySelector("textarea").selectionStart = start;
// the following lines will trigger a select event that should be skipped
if (start !== end) {
internalSelectionEvents += 1;
}
textArea.shadowRoot.querySelector("textarea").selectionEnd = end;
textArea.focus();
const innerTextArea = textArea.shadowRoot.querySelector("textarea");
return {
x: rect.right - innerTextArea.scrollLeft,
y: rect.top - innerTextArea.scrollTop
};
}

const textArea = document.getElementById("basic-textarea")

textArea.onkeyup = function() {
var position = getCaretPosition(textArea);
test.style.left = position.x + 'px';
test.style.top = (position.y + window.scrollY) + 'px';
pop.showAt(test, true);
}


const getSelection = () => {
const selection = window.getSelection();
console.log(selection);
if (selection.anchorNode && selection.anchorNode.nodeType === Node.TEXT_NODE) {
// Firefox
const range = document.createRange();
range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.focusNode, selection.focusOffset);
return range;
}
if (selection.anchorNode) {
// chrome
const node = selection.anchorNode.childNodes[selection.anchorOffset];
if (node.shadowRoot) {
const innerSelection = node.shadowRoot.getSelection();
console.log(innerSelection);
const range = document.createRange();
range.setStart(innerSelection.anchorNode, innerSelection.anchorOffset);
range.setEnd(innerSelection.focusNode, innerSelection.focusOffset);
return range;
}
} else {
// safari
const range = selection.getComposedRanges()[0];
const innerSelection = range.startContainer.childNodes[range.startOffset];
console.log(innerSelection);
const innerSelection2 = document.getSelection().getComposedRanges(innerSelection.shadowRoot)[0];
console.log(innerSelection2);

const range2 = document.createRange();
range2.setStart(innerSelection2.startContainer, innerSelection2.startOffset);
range2.setEnd(innerSelection2.endContainer, innerSelection2.endOffset);
return range2;
}
}

const updateSelection = () => {
const range = getSelection();
console.log({selection});
const {top, left, bottom, right } = range.getBoundingClientRect();
test2.style.top = `${top}px`;
test2.style.left = `${right}px`;
document.getElementById("selection").innerHTML = `${getSelection()}`
}

document.addEventListener("selectionchange", () => {
updateSelection();
});

// hide the caret as it interferes with selection
document.addEventListener("mousedown", () => {
test2.style.display = "none";
});

// show the caret when selection is finished
document.addEventListener("mouseup", () => {
test2.style.display = "block";
});

setTimeout(() => {
const innerTextArea = textArea.shadowRoot.querySelector("textarea");
innerTextArea.addEventListener("select", () => {
if (internalSelectionEvents) {
// was this triggered by the measurement copy? skip it
internalSelectionEvents -= 1;
return;
}
var position = getCaretPosition(textArea);
console.log({position})
test.style.left = position.x + 'px';
test.style.top = (position.y + window.scrollY) + 'px';
pop.showAt(test, true);
})
}, 1000);
</script>

</body>
</html>
Loading