-
-
Notifications
You must be signed in to change notification settings - Fork 28
/
autoresize.js
374 lines (311 loc) · 9.43 KB
/
autoresize.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
import { A } from '@ember/array';
import Mixin from '@ember/object/mixin';
import { once, scheduleOnce } from '@ember/runloop';
import { isEmpty } from '@ember/utils';
import { alias } from '@ember/object/computed';
import { observer, computed, set, get } from '@ember/object';
import { on } from '@ember/object/evented';
import { getStyles, getLayout, measureText } from "dom-ruler";
import fontLoaded from "../system/font-loaded";
import config from 'ember-get-config';
// jQuery is not loaded in fastboot
let trim = function(str) {
if (str !== null && str !== undefined) {
return str.trim();
}
};
function withUnits(number) {
const unitlessNumber = parseFloat(number + '', 10) + '';
if (unitlessNumber === number + '') {
return `${number}px`;
}
return number;
}
/**
This mixin provides common functionality for automatically
resizing view depending on the contents of the view. To
make your view resize, you need to set the `autoresize`
property to `true`, and let the mixin know whether it
can resize the height of width.
In addition, `autoResizeText` is a required property for
this mixin. It is already provided for `Ember.TextField` and
`Ember.TextArea`.
@class AutoResize
@extends Ember.Mixin
@since Ember 1.0.0-rc3
*/
export default Mixin.create({
/**
Add `ember-auto-resize` so additional
styling can be applied indicating that
the text field will automatically resize.
@property classNameBindings
*/
classNameBindings: ['autoresize:ember-auto-resize'],
/**
Whether the view using this mixin should
autoresize it's contents. To enable autoresizing
using the view's default resizing, set
the attribute in your template.
```handlebars
{{input autoresize=true}}
```
@property {boolean} autoresize
@default false
*/
autoresize: computed({
get() {},
set(_, value) {
if (typeof document === 'undefined') {
return false;
}
return value;
}
}),
/**
The DOM element that should be targeted
for autoresizing. This defaults to the
component's element, but may be used
to autoresize a child element in cases
where you may be using raw HTML elements.
This element *must* be observable to trigger
proper resizing.
@property {DOMElement} autoresizeElement
@type DOMElement
*/
autoresizeElement: null,
autoresizeElementDidChange: on('didInsertElement', function () {
set(this, 'autoresizeElement', get(this, 'element'));
}),
/**
The current dimensions of the view being
resized in terms of an object hash that
includes a `width` and `height` property.
@private
@accessor {object} dimensions
@default null
*/
dimensions: null,
/**
Whether the auto resize mixin should resize
the width of this view.
@property {boolean} shouldResizeWidth
@default false
*/
shouldResizeWidth: false,
/**
Whether the auto resize mixin should resize
the height of this view.
@property {boolean} shouldResizeHeight
@default false
*/
shouldResizeHeight: false,
/**
If set, this property will dictate how much
the view is allowed to resize horizontally
until it either falls back to scrolling or
resizing vertically.
@property {number} maxWidth
@default null
*/
maxWidth: alias('max-width'),
/**
If set, this property dictates how much
the view is allowed to resize vertically.
If this is not set and the view is allowed
to resize vertically, it will do so infinitely.
@property {number} maxHeight
@default null
*/
maxHeight: alias('max-height'),
/**
A required property that should alias the
property that should trigger recalculating
the dimensions of the view.
@property {string} autoResizeText
@required
*/
autoResizeText: null,
/**
Whether the autoResizeText has been sanitized
and should be treated as HTML.
@property {boolean} ignoreEscape
@default false
*/
ignoreEscape: false,
/**
Whether whitespace should be treated as significant
contrary to any styles on the view.
@property significantWhitespace
@default false
@type Boolean
*/
significantWhitespace: false,
/**
Schedule measuring the view's size.
This happens automatically when the
`autoResizeText` property changes.
@private
@method scheduleMeasurement
*/
scheduleMeasurement: on('init', observer('autoResizeText', function () {
if (get(this, 'autoresize') && !get(this, 'isDestroyed')) {
once(this, 'measureSize');
}
})),
/**
Detect when a font is loaded and resize the box.
@private
@method fontFamilyLoaded
*/
fontFamilyLoaded: observer('autoresizeElement', function () {
if (!this._loadCustomFont()) {
return;
}
let styles = getStyles(get(this, 'autoresizeElement'));
let fontFamilies = styles.fontFamily.split(',');
A(fontFamilies).forEach((fontFamily) => {
fontLoaded(trim(fontFamily)).then(() => {
this.scheduleMeasurement();
}, function () {});
});
}),
/**
Measures the size of the text of the element.
@private
@method measureSize
*/
measureSize() {
const element = get(this, 'autoresizeElement');
if (element == null) { return; }
const text = get(this, 'autoResizeText');
if (isEmpty(text) || get(this, 'isDestroying')) {
set(this, 'measuredSize', { width: 0, height: 0 });
}
// Provide extra styles that will restrict
// width / height growth
var styles = {};
if (get(this, 'shouldResizeWidth')) {
if (get(this, 'maxWidth') != null) {
styles.maxWidth = withUnits(get(this, 'maxWidth'));
}
} else {
styles.maxWidth = getLayout(element).width + 'px';
}
if (get(this, 'shouldResizeHeight')) {
if (get(this, 'maxHeight') != null) {
styles.maxHeight = withUnits(get(this, 'maxHeight'));
}
} else {
styles.maxHeight = getLayout(element).height + 'px';
}
function measureRows(rows) {
var html = '';
for (var i = 0, len = parseInt(rows, 10); i < len; i++) {
html += '<br>';
}
return measureText(html, styles, { template: element, escape: false }).height;
}
// Handle 'rows' attribute on <textarea>s
if (get(this, 'rows')) {
styles.minHeight = measureRows(get(this, 'rows')) + 'px';
}
// Handle 'max-rows' attribute on <textarea>s
if (get(this, 'max-rows') && get(this, 'maxHeight') == null) {
set(this, 'maxHeight', measureRows(get(this, 'max-rows')));
styles.maxHeight = get(this, 'maxHeight') + 'px';
}
// Force white-space to pre-wrap to make
// whitespace significant
if (get(this, 'significantWhitespace')) {
styles.whiteSpace = 'pre-wrap';
}
// Create a signature so we can cache the max width and height
const signature = styles.maxWidth + styles.maxHeight;
if (signature !== this._signature) {
const maxDimensions = measureText('', {
width: styles.maxWidth,
height: styles.maxHeight,
}, { template: element });
this._signature = signature;
this._maxWidth = maxDimensions.width;
this._maxHeight = maxDimensions.height;
}
const size = measureText(text, styles, {
template: element,
escape: !get(this, 'ignoreEscape'),
});
if (styles.maxWidth) { size.maxWidth = this._maxWidth; }
if (styles.maxHeight) { size.maxHeight = this._maxHeight; }
set(this, 'measuredSize', size);
},
/**
Alter the `dimensions` property of the
view to conform to the measured size of
the view.
@private
@method measuredSizeDidChange
*/
measuredSizeDidChange: observer('measuredSize', 'autoresizeElement', function () {
let size = get(this, 'measuredSize');
if (size == null) { return; }
let { maxWidth, maxHeight } = size;
let layoutDidChange = false;
let dimensions = {};
if (get(this, 'shouldResizeWidth')) {
// Account for off-by-one error in FireFox
// (specifically, input elements have 1px
// of scroll when this isn't applied)
// TODO: sniff for this bug and fix it!
size.width += 1;
if (maxWidth != null &&
size.width > maxWidth) {
dimensions.width = maxWidth;
} else {
dimensions.width = size.width;
}
layoutDidChange = true;
}
if (get(this, 'shouldResizeHeight')) {
if (maxHeight != null &&
size.height > maxHeight) {
dimensions.height = maxHeight;
} else {
dimensions.height = size.height;
}
layoutDidChange = true;
}
set(this, 'dimensions', dimensions);
if (layoutDidChange) {
scheduleOnce('render', this, 'dimensionsDidChange');
}
}),
/**
Retiles the view at the end of the render queue.
@private
@method dimensionsDidChange
*/
dimensionsDidChange() {
var dimensions = get(this, 'dimensions');
var styles = {};
for (let key in dimensions) {
if (!dimensions.hasOwnProperty(key)) { continue; }
styles[key] = dimensions[key] + 'px';
}
if (get(this, 'maxHeight') == null) {
styles.overflow = 'hidden';
}
var element = get(this, 'autoresizeElement');
if (element) {
for(let prop in styles) {
element.style[prop] = styles[prop];
}
}
},
/**
Internal function to read config options
*/
_loadCustomFont() {
return get(config || {}, 'ember-autoresize.customFont') !== false;
},
});