Skip to content

Commit 535f955

Browse files
committed
MC-3939: Can't select or edit text ranges inside of a live edit block
- Strip any HTML on paste - Fix placeholder display in Safari
1 parent 0adde6d commit 535f955

File tree

3 files changed

+105
-38
lines changed

3 files changed

+105
-38
lines changed

app/code/Magento/PageBuilder/view/adminhtml/web/css/source/content-type/_preview.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
i {
1919
font-style: normal;
2020
}
21-
&:empty:before {
21+
&.placeholder-text:before {
2222
content: attr(data-placeholder);
2323
}
2424
}

app/code/Magento/PageBuilder/view/adminhtml/web/js/binding/live-edit.js

Lines changed: 53 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/code/Magento/PageBuilder/view/adminhtml/web/ts/js/binding/live-edit.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,30 @@ import ko from "knockout";
1010
import keyCodes from "Magento_Ui/js/lib/key-codes";
1111
import _ from "underscore";
1212

13+
/**
14+
* Strip HTML and return text
15+
*
16+
* @param {string} html
17+
* @returns {string}
18+
*/
19+
function stripHtml(html: string) {
20+
const tempDiv = document.createElement("div");
21+
22+
tempDiv.innerHTML = html;
23+
return tempDiv.textContent;
24+
}
25+
1326
/**
1427
* Add or remove the placeholder-text class from the element based on its content
1528
*
1629
* @param {Element} element
1730
*/
1831
function handlePlaceholderClass(element: Element) {
19-
if (element.innerHTML.length === 0) {
20-
$(element).addClass("placeholder-text");
32+
if (stripHtml(element.innerHTML).length === 0) {
33+
element.innerHTML = "";
34+
element.classList.add("placeholder-text");
2135
} else {
22-
$(element).removeClass("placeholder-text");
36+
element.classList.remove("placeholder-text");
2337
}
2438
}
2539

@@ -29,31 +43,18 @@ ko.bindingHandlers.liveEdit = {
2943
/**
3044
* Init the live edit binding on an element
3145
*
32-
* @param {any} element
46+
* @param {HTMLElement} element
3347
* @param {() => any} valueAccessor
3448
* @param {KnockoutAllBindingsAccessor} allBindings
3549
* @param {any} viewModel
3650
* @param {KnockoutBindingContext} bindingContext
3751
*/
38-
init(element, valueAccessor, allBindings, viewModel, bindingContext) {
52+
init(element: HTMLElement, valueAccessor, allBindings, viewModel, bindingContext) {
3953
const {field, placeholder, selectAll = false} = valueAccessor();
4054
let focusedValue = element.innerHTML;
4155
let previouslyFocused: boolean = false;
4256
let blurTimeout: number;
4357

44-
/**
45-
* Strip HTML and return text
46-
*
47-
* @param {string} html
48-
* @returns {string}
49-
*/
50-
const stripHtml = (html: string) => {
51-
const tempDiv = document.createElement("div");
52-
53-
tempDiv.innerHTML = html;
54-
return tempDiv.textContent;
55-
};
56-
5758
/**
5859
* Record the value on focus, only conduct an update when data changes
5960
*/
@@ -143,16 +144,47 @@ ko.bindingHandlers.liveEdit = {
143144
handlePlaceholderClass(element);
144145
};
145146

147+
/**
148+
* On paste strip any HTML
149+
*/
150+
const onPaste = () => {
151+
// Record the original caret position so we can ensure we restore it at the correct position
152+
const selection = window.getSelection();
153+
const originalPositionStart = selection.getRangeAt(0).cloneRange().startOffset;
154+
const originalPositionEnd = selection.getRangeAt(0).cloneRange().endOffset;
155+
const originalContentLength = stripHtml(element.innerHTML).length;
156+
// Allow the paste action to update the content
157+
_.defer(() => {
158+
const strippedValue = stripHtml(element.innerHTML);
159+
element.innerHTML = strippedValue;
160+
/**
161+
* Calculate the position the caret should end up at, the difference in string length + the original
162+
* end offset position
163+
*/
164+
let restoredPosition = Math.abs(strippedValue.length - originalContentLength) + originalPositionStart;
165+
// If part of the text was selected adjust the position for the removed text
166+
if (originalPositionStart !== originalPositionEnd) {
167+
restoredPosition += Math.abs(originalPositionEnd - originalPositionStart);
168+
}
169+
const range = document.createRange();
170+
range.setStart(element.childNodes[0], restoredPosition);
171+
range.setEnd(element.childNodes[0], restoredPosition);
172+
selection.removeAllRanges();
173+
selection.addRange(range);
174+
});
175+
};
176+
146177
element.setAttribute("data-placeholder", placeholder);
147178
element.textContent = viewModel.parent.dataStore.get(field);
148-
element.contentEditable = true;
179+
element.contentEditable = "true";
149180
element.addEventListener("focus", onFocus);
150181
element.addEventListener("blur", onBlur);
151182
element.addEventListener("mousedown", onMouseDown);
152183
element.addEventListener("keydown", onKeyDown);
153184
element.addEventListener("keyup", onKeyUp);
154185
element.addEventListener("input", onInput);
155186
element.addEventListener("drop", onDrop);
187+
element.addEventListener("paste", onPaste);
156188

157189
$(element).parent().css("cursor", "text");
158190
handlePlaceholderClass(element);

0 commit comments

Comments
 (0)