jenkins-bot has submitted this change and it was merged.
Change subject: Prevent freezes in Chrome when zoomed in
......................................................................
Prevent freezes in Chrome when zoomed in
Cause: scrollHeight is integer, while borders are non-integer. Thus
the comparison outerHeigh < scrollHeight + borders was always true
causing an infinite loop.
This could have prevented by using for-loop with limited iterations
or fixed by rounding the borders. However, given that it was written
very inefficiently (polling the same unchanged properties again and
again), I decided to replace the custom implementation with a jquery
plugin.
Choosing a correct plugin among the dozens of mostly unmaintained
candidates wasn't easy. We already had one, jquery.autoresize in
our resources. However it seemed to have the same problem and is
essentially unmaintained.
So I replaced jquery.autoresize with jquery.autosize (MIT licence)
and used the new plugin both in the old editor and in the new
editor replacing the custom implementation. The new plugin has
option for padding that I used to leave space for the insertion
buttons. If I remember correctly this was the only reason why we
made a custom implementation in the first place.
Could also add animations for the size changes but that seemed
unnecessary.
There is one behavioral change compared to our custom implementation:
now the text area also shrinks.
Bug: 47635
Change-Id: Idd1d2f819b994045f26ced5c5022efcf5b058116
---
M .jshintignore
M Resources.php
M resources/css/ext.translate.editor.css
M resources/js/ext.translate.editor.js
M resources/js/ext.translate.quickedit.js
D resources/js/jquery.autoresize.js
A resources/js/jquery.autosize.js
7 files changed, 213 insertions(+), 305 deletions(-)
Approvals:
Siebrand: Looks good to me, approved
jenkins-bot: Verified
diff --git a/.jshintignore b/.jshintignore
index 14fcc60..93772b5 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1,3 +1,3 @@
# upstream lib
-resources/js/jquery.autoresize.js
+resources/js/jquery.autosize.js
resources/js/jquery.ui.position.js
diff --git a/Resources.php b/Resources.php
index 17da2a7..aea1f78 100644
--- a/Resources.php
+++ b/Resources.php
@@ -54,6 +54,7 @@
'jquery.makeCollapsible',
'jquery.tipsy',
'jquery.textchange',
+ 'jquery.autosize',
),
'messages' => array(
'tux-status-translated',
@@ -233,7 +234,7 @@
'ext.translate.hooks',
'jquery.form',
'jquery.ui.dialog',
- 'jquery.autoresize',
+ 'jquery.autosize',
'mediawiki.util',
),
) + $resourcePaths;
@@ -387,8 +388,9 @@
'scripts' => 'resources/js/ext.translate.dropdownmenu.js',
) + $resourcePaths;
-$wgResourceModules['jquery.autoresize'] = array(
- 'scripts' => 'resources/js/jquery.autoresize.js',
+// Third party module
+$wgResourceModules['jquery.autosize'] = array(
+ 'scripts' => 'resources/js/jquery.autosize.js',
) + $resourcePaths;
$wgResourceModules['jquery.textchange'] = array(
diff --git a/resources/css/ext.translate.editor.css
b/resources/css/ext.translate.editor.css
index 22e2b68..42efc19 100644
--- a/resources/css/ext.translate.editor.css
+++ b/resources/css/ext.translate.editor.css
@@ -50,6 +50,7 @@
font-size: 16px;
padding: 5px 5px 30px 5px;
height: 100px;
+ min-height: 100px;
overflow-y: auto;
position: relative;
z-index: 100;
diff --git a/resources/js/ext.translate.editor.js
b/resources/js/ext.translate.editor.js
index 3748c36..e904142 100644
--- a/resources/js/ext.translate.editor.js
+++ b/resources/js/ext.translate.editor.js
@@ -438,6 +438,11 @@
$textarea.prop( 'placeholder', mw.msg(
'tux-editor-placeholder' ) );
}
+ // The extra newlines is supposed to leave enough space
for the
+ // insertion buttons. Seems to work as long as all the
buttons
+ // are only in one line.
+ $textarea.autosize( {append: '\n\n\n' } );
+
$textarea.on( 'textchange', function () {
var $textarea = $( this ),
$saveButton =
translateEditor.$editor.find( '.tux-editor-save-button' ),
@@ -447,15 +452,6 @@
if ( original !== '' ) {
$discardChangesButton.removeClass(
'hide' );
- }
-
- // Expand the text area height as content grows
- while ( $textarea.outerHeight() <
- this.scrollHeight +
- parseFloat( $textarea.css(
'borderTopWidth' ) ) +
- parseFloat( $textarea.css(
'borderBottomWidth' ) )
- ) {
- $textarea.height( $textarea.height() +
parseFloat( $textarea.css( 'fontSize' ) ) );
}
/* Avoid Unsaved marking when translated
message is not changed in content.
@@ -470,7 +466,6 @@
translateEditor.dirty = true;
mw.translate.dirty = true;
}
- adjustSize( $textarea );
$saveButton.text( mw.msg(
'tux-editor-save-button-label' ) );
// When there is content in the editor enable
the button.
@@ -843,7 +838,14 @@
this.$messageItem.addClass( 'hide' );
this.$editor.removeClass( 'hide' );
$textarea.focus();
- adjustSize( $textarea );
+
+ // Apparently there is still something going on that
affects the
+ // layout of the text area after this function. Use
very small
+ // delay to have it settle down and have correct
results. Otherwise
+ // there will be a size change once the first letter is
typed.
+ delay( function() {
+ $textarea.trigger( 'autosize' );
+ }, 1 );
this.shown = true;
this.$editTrigger.addClass( 'open' );
@@ -977,21 +979,6 @@
};
$.fn.translateeditor.Constructor = TranslateEditor;
-
- /*
- * Expand the text area height as content grows
- */
- function adjustSize( $textarea ) {
- while ( $textarea.outerHeight() <
- ( $textarea.prop( 'scrollHeight' ) +
- parseFloat( $textarea.css( 'borderTopWidth' ) ) +
- parseFloat( $textarea.css( 'borderBottomWidth' ) ) )
- ) {
- $textarea.height( $textarea.height() +
- parseFloat( $textarea.css( 'fontSize' ) ) +
- parseFloat( $textarea.css( 'paddingBottom' ) )
);
- }
- }
var delay = ( function () {
var timer = 0;
diff --git a/resources/js/ext.translate.quickedit.js
b/resources/js/ext.translate.quickedit.js
index acdea1a..6ece451 100644
--- a/resources/js/ext.translate.quickedit.js
+++ b/resources/js/ext.translate.quickedit.js
@@ -138,7 +138,7 @@
textarea = form.find( '.mw-translate-edit-area' );
textarea.css( 'display', 'block' );
- textarea.autoResize( { maxHeight: 200 } );
+ textarea.autosize();
textarea[0].focus();
if ( form.find( '.mw-translate-messagechecks' ) ) {
diff --git a/resources/js/jquery.autoresize.js
b/resources/js/jquery.autoresize.js
deleted file mode 100644
index 7d415a9..0000000
--- a/resources/js/jquery.autoresize.js
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * jQuery.fn.autoResize 1.14
- * --
- * https://github.com/jamespadolsey/jQuery.fn.autoResize
- * --
- * This program is free software. It comes without any warranty, to
- * the extent permitted by applicable law. You can redistribute it
- * and/or modify it under the terms of the Do What The Fuck You Want
- * To Public License, Version 2, as published by Sam Hocevar. See
- * http://sam.zoy.org/wtfpl/COPYING for more details. */
-
-(function($){
-
- var uid = 'ar' + +new Date,
-
- defaults = autoResize.defaults = {
- onResize: function(){},
- onBeforeResize: function(){ return 123; },
- onAfterResize: function(){ return 555; },
- animate: {
- duration: 200,
- complete: function(){}
- },
- extraSpace: 50,
- minHeight: 'original',
- maxHeight: 500,
- minWidth: 'original',
- maxWidth: 500
- };
-
- autoResize.cloneCSSProperties = [
- 'lineHeight', 'textDecoration', 'letterSpacing',
- 'fontSize', 'fontFamily', 'fontStyle', 'fontWeight',
- 'textTransform', 'textAlign', 'direction', 'wordSpacing',
'fontSizeAdjust',
- 'paddingTop', 'paddingLeft', 'paddingBottom', 'paddingRight',
'width'
- ];
-
- autoResize.cloneCSSValues = {
- position: 'absolute',
- top: -9999,
- left: -9999,
- opacity: 0,
- overflow: 'hidden'
- };
-
- autoResize.resizableFilterSelector = [
- 'textarea:not(textarea.' + uid + ')',
- 'input:not(input[type])',
- 'input[type=text]',
- 'input[type=password]',
- 'input[type=email]',
- 'input[type=url]'
- ].join(',');
-
- autoResize.AutoResizer = AutoResizer;
-
- $.fn.autoResize = autoResize;
-
- function autoResize(config) {
- this.filter(autoResize.resizableFilterSelector).each(function(){
- new AutoResizer( $(this), config );
- });
- return this;
- }
-
- function AutoResizer(el, config) {
-
- if (el.data('AutoResizer')) {
- el.data('AutoResizer').destroy();
- }
-
- config = this.config = $.extend({}, autoResize.defaults,
config);
- this.el = el;
-
- this.nodeName = el[0].nodeName.toLowerCase();
-
- this.originalHeight = el.height();
- this.previousScrollTop = null;
-
- this.value = el.val();
-
- if (config.maxWidth === 'original') { config.maxWidth =
el.width(); }
- if (config.minWidth === 'original') { config.minWidth =
el.width(); }
- if (config.maxHeight === 'original') { config.maxHeight =
el.height(); }
- if (config.minHeight === 'original') { config.minHeight =
el.height(); }
-
- if (this.nodeName === 'textarea') {
- el.css({
- resize: 'none',
- overflowY: 'hidden'
- });
- }
-
- el.data('AutoResizer', this);
-
- // Make sure onAfterResize is called upon animation completion
- config.animate.complete = (function(f){
- return function() {
- config.onAfterResize.call(el);
- return f.apply(this, arguments);
- };
- }(config.animate.complete));
-
- this.bind();
-
- }
-
- AutoResizer.prototype = {
-
- bind: function() {
-
- var check = $.proxy(function(){
- this.check();
- return true;
- }, this);
-
- this.unbind();
-
- this.el
- .bind('keyup.autoResize', check)
- //.bind('keydown.autoResize', check)
- .bind('change.autoResize', check)
- .bind('paste.autoResize', function() {
- setTimeout(function() { check(); }, 0);
- });
-
- if (!this.el.is(':hidden')) {
- this.check(null, true);
- }
-
- },
-
- unbind: function() {
- this.el.unbind('.autoResize');
- },
-
- createClone: function() {
-
- var el = this.el,
- clone = this.nodeName === 'textarea' ?
el.clone() : $('<span>');
-
- this.clone = clone;
-
- $.each(autoResize.cloneCSSProperties, function(i, p){
- clone[0].style[p] = el.css(p);
- });
-
- clone
- .removeAttr('name')
- .removeAttr('id')
- .addClass(uid)
- .attr('tabIndex', -1)
- .css(autoResize.cloneCSSValues);
-
- if (this.nodeName === 'textarea') {
- clone.height('auto');
- } else {
- clone.width('auto').css({
- whiteSpace: 'nowrap'
- });
- }
-
- },
-
- check: function(e, immediate) {
-
- if (!this.clone) {
- this.createClone();
- this.injectClone();
- }
-
- var config = this.config,
- clone = this.clone,
- el = this.el,
- value = el.val();
-
- // Do nothing if value hasn't changed
- if (value === this.prevValue) { return true; }
- this.prevValue = value;
-
- if (this.nodeName === 'input') {
-
- clone.text(value);
-
- // Calculate new width + whether to change
- var cloneWidth = clone.width(),
- newWidth = (cloneWidth +
config.extraSpace) >= config.minWidth ?
- cloneWidth + config.extraSpace
: config.minWidth,
- currentWidth = el.width();
-
- newWidth = Math.min(newWidth, config.maxWidth);
-
- if (
- (newWidth < currentWidth && newWidth >=
config.minWidth) ||
- (newWidth >= config.minWidth &&
newWidth <= config.maxWidth)
- ) {
-
- config.onBeforeResize.call(el);
- config.onResize.call(el);
-
- el.scrollLeft(0);
-
- if (config.animate && !immediate) {
- el.stop(1,1).animate({
- width: newWidth
- }, config.animate);
- } else {
- el.width(newWidth);
- config.onAfterResize.call(el);
- }
-
- }
-
- return;
-
- }
-
- // TEXTAREA
-
-
clone.width(el.width()).height(0).val(value).scrollTop(10000);
-
- var scrollTop = clone[0].scrollTop;
-
- // Don't do anything if scrollTop hasen't changed:
- if (this.previousScrollTop === scrollTop) {
- return;
- }
-
- this.previousScrollTop = scrollTop;
-
- if (scrollTop + config.extraSpace >= config.maxHeight) {
- el.css('overflowY', '');
- scrollTop = config.maxHeight;
- immediate = true;
- } else if (scrollTop <= config.minHeight) {
- scrollTop = config.minHeight;
- } else {
- el.css('overflowY', 'hidden');
- scrollTop += config.extraSpace;
- }
-
- config.onBeforeResize.call(el);
- config.onResize.call(el);
-
- // Either animate or directly apply height:
- if (config.animate && !immediate) {
- el.stop(1,1).animate({
- height: scrollTop
- }, config.animate);
- } else {
- el.height(scrollTop);
- config.onAfterResize.call(el);
- }
-
- },
-
- destroy: function() {
- this.unbind();
- this.el.removeData('AutoResizer');
- this.clone.remove();
- delete this.el;
- delete this.clone;
- },
-
- injectClone: function() {
- (
- autoResize.cloneContainer ||
- (autoResize.cloneContainer =
$('<arclones/>').appendTo('body'))
- ).append(this.clone);
- }
-
- };
-
-})(jQuery);
diff --git a/resources/js/jquery.autosize.js b/resources/js/jquery.autosize.js
new file mode 100644
index 0000000..c7ae56f
--- /dev/null
+++ b/resources/js/jquery.autosize.js
@@ -0,0 +1,192 @@
+/*!
+ jQuery Autosize v1.16.7
+ (c) 2013 Jack Moore - jacklmoore.com
+ updated: 2013-03-20
+ license: http://www.opensource.org/licenses/mit-license.php
+*/
+
+
+(function ($) {
+ var
+ defaults = {
+ className: 'autosizejs',
+ append: '',
+ callback: false
+ },
+ hidden = 'hidden',
+ borderBox = 'border-box',
+ lineHeight = 'lineHeight',
+ supportsScrollHeight,
+
+ // border:0 is unnecessary, but avoids a bug in FireFox on OSX
(http://www.jacklmoore.com/autosize#comment-851)
+ copy = '<textarea tabindex="-1" style="position:absolute; top:-999px;
left:0; right:auto; bottom:auto; border:0; -moz-box-sizing:content-box;
-webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word;
height:0 !important; min-height:0 !important; overflow:hidden;"/>',
+
+ // line-height is conditionally included because IE7/IE8/old Opera do
not return the correct value.
+ copyStyle = [
+ 'fontFamily',
+ 'fontSize',
+ 'fontWeight',
+ 'fontStyle',
+ 'letterSpacing',
+ 'textTransform',
+ 'wordSpacing',
+ 'textIndent'
+ ],
+ oninput = 'oninput',
+ onpropertychange = 'onpropertychange',
+
+ // to keep track which textarea is being mirrored when adjust() is
called.
+ mirrored,
+
+ // the mirror element, which is used to calculate what size the
mirrored element should be.
+ mirror = $(copy).data('autosize', true)[0];
+
+ // test that line-height can be accurately copied.
+ mirror.style.lineHeight = '99px';
+ if ($(mirror).css(lineHeight) === '99px') {
+ copyStyle.push(lineHeight);
+ }
+ mirror.style.lineHeight = '';
+
+ $.fn.autosize = function (options) {
+ options = $.extend({}, defaults, options || {});
+
+ if (mirror.parentNode !== document.body) {
+ $(document.body).append(mirror);
+
+ mirror.value = "\n\n\n";
+ mirror.scrollTop = 9e4;
+ supportsScrollHeight = mirror.scrollHeight ===
mirror.scrollTop + mirror.clientHeight;
+ }
+
+ return this.each(function () {
+ var
+ ta = this,
+ $ta = $(ta),
+ minHeight,
+ active,
+ resize,
+ boxOffset = 0,
+ callback = $.isFunction(options.callback);
+
+ if ($ta.data('autosize')) {
+ // exit if autosize has already been applied,
or if the textarea is the mirror element.
+ return;
+ }
+
+ if ($ta.css('box-sizing') === borderBox ||
$ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') ===
borderBox){
+ boxOffset = $ta.outerHeight() - $ta.height();
+ }
+
+ minHeight = Math.max(parseInt($ta.css('minHeight'), 10)
- boxOffset, $ta.height());
+
+ resize = ($ta.css('resize') === 'none' ||
$ta.css('resize') === 'vertical') ? 'none' : 'horizontal';
+
+ $ta.css({
+ overflow: hidden,
+ overflowY: hidden,
+ wordWrap: 'break-word',
+ resize: resize
+ }).data('autosize', true);
+
+ function initMirror() {
+ mirrored = ta;
+ mirror.className = options.className;
+
+ // mirror is a duplicate textarea located
off-screen that
+ // is automatically updated to contain the same
text as the
+ // original textarea. mirror always has a
height of 0.
+ // This gives a cross-browser supported way
getting the actual
+ // height of the text, through the scrollTop
property.
+ $.each(copyStyle, function(i, val){
+ mirror.style[val] = $ta.css(val);
+ });
+ }
+
+ // Using mainly bare JS in this function because it is
going
+ // to fire very often while typing, and needs to very
efficient.
+ function adjust() {
+ var height, overflow, original;
+
+ if (mirrored !== ta) {
+ initMirror();
+ }
+
+ // the active flag keeps IE from tripping all
over itself. Otherwise
+ // actions in the adjust function will cause IE
to call adjust again.
+ if (!active) {
+ active = true;
+ mirror.value = ta.value +
options.append;
+ mirror.style.overflowY =
ta.style.overflowY;
+ original = parseInt(ta.style.height,10);
+
+ // Update the width in case the
original textarea width has changed
+ // A floor of 0 is needed because IE8
returns a negative value for hidden textareas, raising an error.
+ mirror.style.width =
Math.max($ta.width(), 0) + 'px';
+
+ if (supportsScrollHeight) {
+ height = mirror.scrollHeight;
+ } else { // IE6 & IE7
+ mirror.scrollTop = 0;
+ mirror.scrollTop = 9e4;
+ height = mirror.scrollTop;
+ }
+
+ var maxHeight =
parseInt($ta.css('maxHeight'), 10);
+ // Opera returns '-1px' when max-height
is set to 'none'.
+ maxHeight = maxHeight && maxHeight > 0
? maxHeight : 9e4;
+ if (height > maxHeight) {
+ height = maxHeight;
+ overflow = 'scroll';
+ } else if (height < minHeight) {
+ height = minHeight;
+ }
+ height += boxOffset;
+ ta.style.overflowY = overflow || hidden;
+
+ if (original !== height) {
+ ta.style.height = height + 'px';
+ if (callback) {
+
options.callback.call(ta);
+ }
+ }
+
+ // This small timeout gives IE a chance
to draw it's scrollbar
+ // before adjust can be run again
(prevents an infinite loop).
+ setTimeout(function () {
+ active = false;
+ }, 1);
+ }
+ }
+
+ if (onpropertychange in ta) {
+ if (oninput in ta) {
+ // Detects IE9. IE9 does not fire
onpropertychange or oninput for deletions,
+ // so binding to onkeyup to catch most
of those occassions. There is no way that I
+ // know of to detect something like
'cut' in IE9.
+ ta[oninput] = ta.onkeyup = adjust;
+ } else {
+ // IE7 / IE8
+ ta[onpropertychange] = adjust;
+ }
+ } else {
+ // Modern Browsers
+ ta[oninput] = adjust;
+ }
+
+ $(window).on('resize', function(){
+ active = false;
+ adjust();
+ });
+
+ // Allow for manual triggering if needed.
+ $ta.on('autosize', function(){
+ active = false;
+ adjust();
+ });
+
+ // Call adjust in case the textarea already contains
text.
+ adjust();
+ });
+ };
+}(window.jQuery || window.Zepto));
--
To view, visit https://gerrit.wikimedia.org/r/60969
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Idd1d2f819b994045f26ced5c5022efcf5b058116
Gerrit-PatchSet: 4
Gerrit-Project: mediawiki/extensions/Translate
Gerrit-Branch: master
Gerrit-Owner: Nikerabbit <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits