зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1143742 - part2: multiline inplace-editor should support a maxWidth option;r=gl
The inplaceEditor now supports a maxWidth configuration option which can either be a number or a method returning a number. This maxWidth will be applied to the hidden element used in order to autosize the input. MozReview-Commit-ID: JTiCQ3HK5bn --HG-- extra : rebase_source : dcf7ba4a897cd77b43b333ec3b5633dc9043e51d extra : source : a93558488cf7fc9f54165bd5f98055e8a3901dac
This commit is contained in:
Родитель
2748597a5d
Коммит
93a3e46f97
|
@ -2518,6 +2518,11 @@ function TextEditor(container, node, template) {
|
|||
stopOnReturn: true,
|
||||
trigger: "dblclick",
|
||||
multiline: true,
|
||||
maxWidth: () => {
|
||||
let elementRect = this.value.getBoundingClientRect();
|
||||
let containerRect = this.container.elt.getBoundingClientRect();
|
||||
return containerRect.right - elementRect.left - 2;
|
||||
},
|
||||
trimOutput: false,
|
||||
done: (val, commit) => {
|
||||
if (!commit) {
|
||||
|
|
|
@ -233,7 +233,9 @@ TextPropertyEditor.prototype = {
|
|||
advanceChars: advanceValidate,
|
||||
contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
|
||||
property: this.prop,
|
||||
popup: this.popup
|
||||
popup: this.popup,
|
||||
multiline: true,
|
||||
maxWidth: () => this.container.getBoundingClientRect().width
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -91,6 +91,10 @@ const { findMostRelevantCssPropertyIndex } = require("./suggestion-picker");
|
|||
* defaults to "click"
|
||||
* {Boolean} multiline: Should the editor be a multiline textarea?
|
||||
* defaults to false
|
||||
* {Function or Number} maxWidth:
|
||||
* Should the editor wrap to remain below the provided max width. Only
|
||||
* available if multiline is true. If a function is provided, it will be
|
||||
* called when replacing the element by the inplace input.
|
||||
* {Boolean} trimOutput: Should the returned string be trimmed?
|
||||
* defaults to true
|
||||
* {Boolean} preserveTextStyles: If true, do not copy text-related styles
|
||||
|
@ -199,6 +203,11 @@ function InplaceEditor(options, event) {
|
|||
this.destroy = options.destroy;
|
||||
this.initial = options.initial ? options.initial : this.elt.textContent;
|
||||
this.multiline = options.multiline || false;
|
||||
this.maxWidth = options.maxWidth;
|
||||
if (typeof this.maxWidth == "function") {
|
||||
this.maxWidth = this.maxWidth();
|
||||
}
|
||||
|
||||
this.trimOutput = options.trimOutput === undefined
|
||||
? true
|
||||
: !!options.trimOutput;
|
||||
|
@ -218,9 +227,16 @@ function InplaceEditor(options, event) {
|
|||
this._onKeyup = this._onKeyup.bind(this);
|
||||
|
||||
this._createInput();
|
||||
this._autosize();
|
||||
this.inputCharWidth = this._getInputCharWidth();
|
||||
|
||||
// Hide the provided element and add our editor.
|
||||
this.originalDisplay = this.elt.style.display;
|
||||
this.elt.style.display = "none";
|
||||
this.elt.parentNode.insertBefore(this.input, this.elt);
|
||||
|
||||
// After inserting the input to have all CSS styles applied, start autosizing.
|
||||
this._autosize();
|
||||
|
||||
this.inputCharDimensions = this._getInputCharDimensions();
|
||||
// Pull out character codes for advanceChars, listing the
|
||||
// characters that should trigger a blur.
|
||||
if (typeof options.advanceChars === "function") {
|
||||
|
@ -234,11 +250,6 @@ function InplaceEditor(options, event) {
|
|||
this._advanceChars = charCode => charCode in advanceCharcodes;
|
||||
}
|
||||
|
||||
// Hide the provided element and add our editor.
|
||||
this.originalDisplay = this.elt.style.display;
|
||||
this.elt.style.display = "none";
|
||||
this.elt.parentNode.insertBefore(this.input, this.elt);
|
||||
|
||||
this.input.focus();
|
||||
|
||||
if (typeof options.selectAll == "undefined" || options.selectAll) {
|
||||
|
@ -286,6 +297,13 @@ InplaceEditor.prototype = {
|
|||
this.input =
|
||||
this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
|
||||
this.input.inplaceEditor = this;
|
||||
|
||||
if (this.multiline) {
|
||||
// Hide the textarea resize handle.
|
||||
this.input.style.resize = "none";
|
||||
this.input.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
this.input.classList.add("styleinspector-propertyeditor");
|
||||
this.input.value = this.initial;
|
||||
if (!this.preserveTextStyles) {
|
||||
|
@ -352,6 +370,18 @@ InplaceEditor.prototype = {
|
|||
style.position = "absolute";
|
||||
style.top = "0";
|
||||
style.left = "0";
|
||||
|
||||
if (this.multiline) {
|
||||
style.whiteSpace = "pre-wrap";
|
||||
style.wordWrap = "break-word";
|
||||
if (this.maxWidth) {
|
||||
style.maxWidth = this.maxWidth + "px";
|
||||
// Use position fixed to measure dimensions without any influence from
|
||||
// the container of the editor.
|
||||
style.position = "fixed";
|
||||
}
|
||||
}
|
||||
|
||||
copyTextStyles(this.input, this._measurement);
|
||||
this._updateSize();
|
||||
},
|
||||
|
@ -374,36 +404,49 @@ InplaceEditor.prototype = {
|
|||
// Replace spaces with non-breaking spaces. Otherwise setting
|
||||
// the span's textContent will collapse spaces and the measurement
|
||||
// will be wrong.
|
||||
this._measurement.textContent = this.input.value.replace(/ /g, "\u00a0");
|
||||
let content = this.input.value;
|
||||
let unbreakableSpace = "\u00a0";
|
||||
|
||||
let width = this._measurement.offsetWidth;
|
||||
// Make sure the content is not empty.
|
||||
if (content === "") {
|
||||
content = unbreakableSpace;
|
||||
}
|
||||
|
||||
// If content ends with a new line, add a blank space to force the autosize
|
||||
// element to adapt its height.
|
||||
if (content.lastIndexOf("\n") === content.length - 1) {
|
||||
content = content + unbreakableSpace;
|
||||
}
|
||||
|
||||
if (!this.multiline) {
|
||||
content = content.replace(/ /g, unbreakableSpace);
|
||||
}
|
||||
|
||||
this._measurement.textContent = content;
|
||||
|
||||
// Do not use offsetWidth: it will round floating width values.
|
||||
let width = this._measurement.getBoundingClientRect().width + 2;
|
||||
if (this.multiline) {
|
||||
// Make sure there's some content in the current line. This is a hack to
|
||||
// account for the fact that after adding a newline the <pre> doesn't grow
|
||||
// unless there's text content on the line.
|
||||
width += 15;
|
||||
this.input.style.height = this._measurement.offsetHeight + "px";
|
||||
}
|
||||
|
||||
if (width === 0) {
|
||||
// If the editor is empty use a width corresponding to 1 character.
|
||||
this.input.style.width = "1ch";
|
||||
} else {
|
||||
// Add 2 pixels to ensure the caret will be visible
|
||||
width = width + 2;
|
||||
this.input.style.width = width + "px";
|
||||
if (this.maxWidth) {
|
||||
width = Math.min(this.maxWidth, width);
|
||||
}
|
||||
let height = this._measurement.getBoundingClientRect().height;
|
||||
this.input.style.height = height + "px";
|
||||
}
|
||||
this.input.style.width = width + "px";
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the width of a single character in the input to properly position the
|
||||
* autocompletion popup.
|
||||
* Get the width and height of a single character in the input to properly
|
||||
* position the autocompletion popup.
|
||||
*/
|
||||
_getInputCharWidth: function() {
|
||||
// Just make the text content to be 'x' to get the width of any character in
|
||||
// a monospace font.
|
||||
_getInputCharDimensions: function() {
|
||||
// Just make the text content to be 'x' to get the width and height of any
|
||||
// character in a monospace font.
|
||||
this._measurement.textContent = "x";
|
||||
return this._measurement.offsetWidth;
|
||||
let width = this._measurement.clientWidth;
|
||||
let height = this._measurement.clientHeight;
|
||||
return { width, height };
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1307,10 +1350,54 @@ InplaceEditor.prototype = {
|
|||
function copyTextStyles(from, to) {
|
||||
let win = from.ownerDocument.defaultView;
|
||||
let style = win.getComputedStyle(from);
|
||||
to.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
|
||||
to.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
|
||||
to.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
|
||||
to.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
|
||||
let getCssText = name => style.getPropertyCSSValue(name).cssText;
|
||||
|
||||
to.style.fontFamily = getCssText("font-family");
|
||||
to.style.fontSize = getCssText("font-size");
|
||||
to.style.fontWeight = getCssText("font-weight");
|
||||
to.style.fontStyle = getCssText("font-style");
|
||||
to.style.lineHeight = getCssText("line-height");
|
||||
|
||||
// If box-sizing is set to border-box, box model styles also need to be
|
||||
// copied.
|
||||
let boxSizing = getCssText("box-sizing");
|
||||
if (boxSizing === "border-box") {
|
||||
to.style.boxSizing = boxSizing;
|
||||
copyBoxModelStyles(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy box model styles that can impact width and height measurements when box-
|
||||
* sizing is set to "border-box" instead of "content-box".
|
||||
*
|
||||
* @param {DOMNode} from
|
||||
* the element from which styles are copied
|
||||
* @param {DOMNode} to
|
||||
* the element on which copied styles are applied
|
||||
*/
|
||||
function copyBoxModelStyles(from, to) {
|
||||
let win = from.ownerDocument.defaultView;
|
||||
let style = win.getComputedStyle(from);
|
||||
let getCssText = name => style.getPropertyCSSValue(name).cssText;
|
||||
|
||||
// Copy all paddings.
|
||||
to.style.paddingTop = getCssText("padding-top");
|
||||
to.style.paddingRight = getCssText("padding-right");
|
||||
to.style.paddingBottom = getCssText("padding-bottom");
|
||||
to.style.paddingLeft = getCssText("padding-left");
|
||||
|
||||
// Copy border styles.
|
||||
to.style.borderTopStyle = getCssText("border-top-style");
|
||||
to.style.borderRightStyle = getCssText("border-right-style");
|
||||
to.style.borderBottomStyle = getCssText("border-bottom-style");
|
||||
to.style.borderLeftStyle = getCssText("border-left-style");
|
||||
|
||||
// Copy border widths.
|
||||
to.style.borderTopWidth = getCssText("border-top-width");
|
||||
to.style.borderRightWidth = getCssText("border-right-width");
|
||||
to.style.borderBottomWidth = getCssText("border-bottom-width");
|
||||
to.style.borderLeftWidth = getCssText("border-left-width");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -112,6 +112,7 @@ skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
|
|||
skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
|
||||
[browser_inplace-editor-01.js]
|
||||
[browser_inplace-editor-02.js]
|
||||
[browser_inplace-editor_maxwidth.js]
|
||||
[browser_layoutHelpers.js]
|
||||
skip-if = e10s # Layouthelpers test should not run in a content page.
|
||||
[browser_layoutHelpers-getBoxQuads.js]
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
var { editableField } = require("devtools/client/shared/inplace-editor");
|
||||
|
||||
const LINE_HEIGHT = 15;
|
||||
const MAX_WIDTH = 300;
|
||||
const START_TEXT = "Start text";
|
||||
const LONG_TEXT = "I am a long text and I will not fit in a 300px container. " +
|
||||
"I expect the inplace editor to wrap.";
|
||||
|
||||
// Test the inplace-editor behavior with a maxWidth configuration option
|
||||
// defined.
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab("data:text/html;charset=utf-8,inplace editor max width tests");
|
||||
let [host, , doc] = yield createHost();
|
||||
|
||||
info("Testing the maxWidth option in pixels, to precisely check the size");
|
||||
yield new Promise(resolve => {
|
||||
createInplaceEditorAndClick({
|
||||
multiline: true,
|
||||
maxWidth: MAX_WIDTH,
|
||||
start: testMaxWidth,
|
||||
done: resolve
|
||||
}, doc);
|
||||
});
|
||||
|
||||
host.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
let testMaxWidth = Task.async(function* (editor) {
|
||||
is(editor.input.value, START_TEXT, "Span text content should be used");
|
||||
ok(editor.input.offsetWidth < MAX_WIDTH,
|
||||
"Input width should be strictly smaller than MAX_WIDTH");
|
||||
is(getLines(editor.input), 1, "Input should display 1 line of text");
|
||||
|
||||
info("Check a text is on several lines if it does not fit MAX_WIDTH");
|
||||
for (let key of LONG_TEXT) {
|
||||
EventUtils.sendChar(key);
|
||||
checkScrollbars(editor.input);
|
||||
}
|
||||
|
||||
is(editor.input.value, LONG_TEXT, "Long text should be the input value");
|
||||
is(editor.input.offsetWidth, MAX_WIDTH,
|
||||
"Input width should be the same as MAX_WIDTH");
|
||||
is(getLines(editor.input), 3, "Input should display 3 lines of text");
|
||||
checkScrollbars(editor.input);
|
||||
|
||||
info("Delete all characters on line 3.");
|
||||
while (getLines(editor.input) === 3) {
|
||||
EventUtils.sendKey("BACK_SPACE");
|
||||
checkScrollbars(editor.input);
|
||||
}
|
||||
|
||||
is(editor.input.offsetWidth, MAX_WIDTH,
|
||||
"Input width should be the same as MAX_WIDTH");
|
||||
is(getLines(editor.input), 2, "Input should display 2 lines of text");
|
||||
checkScrollbars(editor.input);
|
||||
|
||||
info("Delete all characters on line 2.");
|
||||
while (getLines(editor.input) === 2) {
|
||||
EventUtils.sendKey("BACK_SPACE");
|
||||
checkScrollbars(editor.input);
|
||||
}
|
||||
|
||||
is(getLines(editor.input), 1, "Input should display 1 line of text");
|
||||
checkScrollbars(editor.input);
|
||||
|
||||
info("Delete all characters.");
|
||||
while (editor.input.value !== "") {
|
||||
EventUtils.sendKey("BACK_SPACE");
|
||||
checkScrollbars(editor.input);
|
||||
}
|
||||
|
||||
ok(editor.input.offsetWidth < MAX_WIDTH,
|
||||
"Input width should again be strictly smaller than MAX_WIDTH");
|
||||
ok(editor.input.offsetWidth > 0,
|
||||
"Even with no content, the input has a non-zero width");
|
||||
is(getLines(editor.input), 1, "Input should display 1 line of text");
|
||||
checkScrollbars(editor.input);
|
||||
|
||||
info("Leave the inplace-editor");
|
||||
EventUtils.sendKey("RETURN");
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve the current number of lines displayed in the provided textarea.
|
||||
*
|
||||
* @param {DOMNode} textarea
|
||||
* @return {Number} the number of lines
|
||||
*/
|
||||
function getLines(textarea) {
|
||||
return Math.floor(textarea.clientHeight / LINE_HEIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the provided textarea has no vertical or horizontal scrollbar.
|
||||
*
|
||||
* @param {DOMNode} textarea
|
||||
*/
|
||||
function checkScrollbars(textarea) {
|
||||
is(textarea.scrollHeight, textarea.clientHeight,
|
||||
"Textarea should never have vertical scrollbars");
|
||||
is(textarea.scrollWidth, textarea.clientWidth,
|
||||
"Textarea should never have horizontal scrollbars");
|
||||
}
|
||||
|
||||
function createInplaceEditorAndClick(options, doc) {
|
||||
doc.body.innerHTML = "";
|
||||
let span = options.element = createSpan(doc);
|
||||
|
||||
info("Creating an inplace-editor field");
|
||||
editableField(options);
|
||||
|
||||
info("Clicking on the inplace-editor field to turn to edit mode");
|
||||
span.click();
|
||||
}
|
||||
|
||||
function createSpan(doc) {
|
||||
info("Creating a new span element");
|
||||
let span = doc.createElement("span");
|
||||
span.setAttribute("tabindex", "0");
|
||||
span.style.lineHeight = LINE_HEIGHT + "px";
|
||||
span.style.fontSize = "11px";
|
||||
span.style.fontFamily = "monospace";
|
||||
span.textContent = START_TEXT;
|
||||
doc.body.appendChild(span);
|
||||
return span;
|
||||
}
|
Загрузка…
Ссылка в новой задаче