replace textarea with a content-editable div

Mainly because of the sluggish auto-resize "feature" which comes for
free when using a content-editable div.

If you use a custom CSS, make sure you replace textarea (element with
.textarea (class) and set `white-space: pre`.
This commit is contained in:
Martin Zimmermann 2014-03-27 10:10:28 +01:00
parent 638ddc6359
commit 09451ff707
7 changed files with 93 additions and 57 deletions

View File

@ -27,6 +27,16 @@ a {
font-weight: bold; font-weight: bold;
font-family: "Helvetica", Arial, sans-serif; font-family: "Helvetica", Arial, sans-serif;
} }
.textarea {
white-space: pre;
outline: 0px solid transparent;
}
.textarea.placeholder {
color: #AAA;
}
} }
.parent-highlight { .parent-highlight {
@ -82,6 +92,10 @@ a {
} }
} }
> .textarea-wrapper .textarea {
margin-top: 0.2em;
}
> div.text { > div.text {
p { p {
@ -98,9 +112,10 @@ a {
} }
> div.textarea-wrapper textarea { > div.textarea-wrapper .textarea {
@include fill-parent; @include fill-parent;
@include isso-shadow; @include isso-shadow;
min-height: 48px;
font: inherit; font: inherit;
} }
@ -155,12 +170,9 @@ a {
> .form-wrapper { > .form-wrapper {
@include span-columns(10 of 11); @include span-columns(10 of 11);
textarea { .textarea {
@include fill-parent; @include fill-parent;
@include isso-shadow; @include isso-shadow;
@include placeholder {
color: #AAA;
}
min-height: 48px; min-height: 48px;
font: inherit; font: inherit;

View File

@ -15,7 +15,7 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
$(".avatar > svg", el).replace(lib.identicons.blank(4, 48)); $(".avatar > svg", el).replace(lib.identicons.blank(4, 48));
// on text area focus, generate identicon from IP address // on text area focus, generate identicon from IP address
$(".textarea-wrapper > textarea", el).on("focus", function() { $(".textarea-wrapper > .textarea", el).on("focus", function() {
if ($(".avatar svg", el).getAttribute("className") === "blank") { if ($(".avatar svg", el).getAttribute("className") === "blank") {
$(".avatar svg", el).replace( $(".avatar svg", el).replace(
lib.identicons.generate(lib.pbkdf2(api.remote_addr(), api.salt, 1000, 6), 4, 48)); lib.identicons.generate(lib.pbkdf2(api.remote_addr(), api.salt, 1000, 6), 4, 48));
@ -45,8 +45,10 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
el.onsuccess = function() {}; el.onsuccess = function() {};
el.validate = function() { el.validate = function() {
if ($("textarea", this).value.length < 3) { if (utils.text($(".textarea", this).innerHTML).length < 3 ||
$("textarea", this).focus(); $(".textarea", this).classList.contains("placeholder"))
{
$(".textarea", this).focus();
return false; return false;
} }
return true; return true;
@ -62,14 +64,13 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
api.create($("#isso-thread").getAttribute("data-isso-id"), { api.create($("#isso-thread").getAttribute("data-isso-id"), {
author: $("[name=author]", el).value || null, author: $("[name=author]", el).value || null,
email: $("[name=email]", el).value || null, email: $("[name=email]", el).value || null,
text: $("textarea", el).value, text: utils.text($(".textarea", el).innerHTML),
parent: parent || null parent: parent || null
}).then(function(comment) { }).then(function(comment) {
$("[name=author]", el).value = ""; $("[name=author]", el).value = "";
$("[name=email]", el).value = ""; $("[name=email]", el).value = "";
$("textarea", el).value = ""; $(".textarea", el).innerHTML = "";
$("textarea", el).rows = 2; $(".textarea", el).blur();
$("textarea", el).blur();
insert(comment, true); insert(comment, true);
if (parent !== null) { if (parent !== null) {
@ -79,8 +80,7 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
}); });
}); });
// copy'n'paste sluggy automagically dynamic textarea resize lib.editorify($(".textarea", el));
lib.fancy.autoresize($("textarea", el), 48);
return el; return el;
}; };
@ -136,7 +136,7 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
function(toggler) { function(toggler) {
form = footer.insertAfter(new Postbox(comment.id)); form = footer.insertAfter(new Postbox(comment.id));
form.onsuccess = function() { toggler.next(); }; form.onsuccess = function() { toggler.next(); };
$("textarea", form).focus(); $(".textarea", form).focus();
$("a.reply", footer).textContent = msgs["comment-close"]; $("a.reply", footer).textContent = msgs["comment-close"];
}, },
function() { function() {
@ -186,34 +186,44 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
edit.textContent = msgs["comment-save"]; edit.textContent = msgs["comment-save"];
edit.insertAfter($.new("a.cancel", msgs["comment-cancel"])).on("click", function() { edit.insertAfter($.new("a.cancel", msgs["comment-cancel"])).on("click", function() {
text.textContent = ""; toggler.canceled = true;
text.className = "text";
text.append(comment.text);
toggler.next(); toggler.next();
}); });
toggler.canceled = false;
api.view(comment.id, 1).then(function(rv) { api.view(comment.id, 1).then(function(rv) {
var textarea = $.new("textarea", rv.text); var textarea = lib.editorify($.new("div.textarea"));
lib.fancy.autoresize(textarea, 48);
text.className = "textarea-wrapper"; textarea.textContent = rv.text;
textarea.focus();
text.classList.remove("text");
text.classList.add("textarea-wrapper");
text.textContent = ""; text.textContent = "";
text.append(textarea); text.append(textarea);
textarea.focus();
}); });
}, },
function(toggler) { function(toggler) {
var textarea = $("textarea", text); var textarea = $(".textarea", text);
if (textarea && textarea.value.length < 3) {
if (! toggler.canceled && textarea !== null) {
if (utils.text(textarea.innerHTML).length < 3) {
textarea.focus(); textarea.focus();
toggler.wait(); toggler.wait();
return; return;
} else if (textarea) { } else {
api.modify(comment.id, {"text": textarea.value}).then(function(rv) { api.modify(comment.id, {"text": utils.text(textarea.innerHTML)}).then(function(rv) {
text.innerHTML = rv.text; text.innerHTML = rv.text;
text.className = "text";
comment.text = rv.text; comment.text = rv.text;
}); });
} }
} else {
text.innerHTML = comment.text;
}
text.classList.remove("textarea-wrapper");
text.classList.add("text");
$("a.cancel", footer).remove(); $("a.cancel", footer).remove();
$("a.edit", footer).textContent = msgs["comment-edit"]; $("a.edit", footer).textContent = msgs["comment-edit"];

View File

@ -1,6 +1,6 @@
define(function (require) { define(function (require) {
return { return {
fancy: require("app/lib/fancy"), editorify: require("app/lib/editor"),
identicons: require("app/lib/identicons"), identicons: require("app/lib/identicons"),
pbkdf2: require("app/lib/pbkdf2"), pbkdf2: require("app/lib/pbkdf2"),
sha1: require("app/lib/sha1") sha1: require("app/lib/sha1")

24
isso/js/app/lib/editor.js Normal file
View File

@ -0,0 +1,24 @@
define(["app/dom", "app/markup"], function($, Mark) {
"use strict";
return function(el) {
el.setAttribute("contentEditable", true);
el.on("focus", function() {
if (el.classList.contains("placeholder")) {
el.innerHTML = "";
el.classList.remove("placeholder");
}
});
el.on("blur", function() {
if (el.textContent.length === 0) {
el.textContent = Mark.up("{{ i18n-postbox-text }}");
el.classList.add("placeholder");
}
});
return el;
};
});

View File

@ -1,20 +0,0 @@
define(function() {
"use strict";
var autoresize = function(textarea, minheight) {
var offset= !window.opera ? (textarea.offsetHeight - textarea.clientHeight) : (textarea.offsetHeight + parseInt(window.getComputedStyle(textarea, null).getPropertyValue('border-top-width')));
["keyup", "focus"].forEach(function(event) {
textarea.on(event, function() {
if ((textarea.scrollHeight + offset ) > minheight) {
textarea.style.height = "auto";
textarea.style.height = (textarea.scrollHeight + offset ) + 'px';
}
});
});
};
return {
autoresize: autoresize
};
});

View File

@ -4,7 +4,7 @@
</div> </div>
<div class="form-wrapper"> <div class="form-wrapper">
<div class="textarea-wrapper"> <div class="textarea-wrapper">
<textarea name="text" rows="2" placeholder="{{ i18n-postbox-text }}"></textarea> <div class="textarea placeholder" contenteditable="true">{{ i18n-postbox-text }}</div>
</div> </div>
<section class="auth-section"> <section class="auth-section">
<p class="input-wrapper"> <p class="input-wrapper">

View File

@ -1,4 +1,5 @@
define(["app/markup"], function(Mark) { define(["app/markup"], function(Mark) {
"use strict";
// return `cookie` string if set // return `cookie` string if set
var cookie = function(cookie) { var cookie = function(cookie) {
@ -7,7 +8,7 @@ define(["app/markup"], function(Mark) {
var ago = function(localTime, date) { var ago = function(localTime, date) {
var secs = ((localTime.getTime() - date.getTime()) / 1000) var secs = ((localTime.getTime() - date.getTime()) / 1000);
if (isNaN(secs) || secs < 0 ) { if (isNaN(secs) || secs < 0 ) {
secs = 0; secs = 0;
@ -39,8 +40,17 @@ define(["app/markup"], function(Mark) {
i18n("date-year", Math.ceil(days / 365.25)); i18n("date-year", Math.ceil(days / 365.25));
}; };
var text = function(html) {
var _ = document.createElement("div");
_.innerHTML = html.replace(/<div><br><\/div>/gi, '<br>')
.replace(/<div>/gi,'<br>')
.replace(/<br>/gi, '\n');
return _.textContent.trim();
};
return { return {
cookie: cookie, cookie: cookie,
ago: ago ago: ago,
text: text
}; };
}); });