blob: 0b848196fd9929c468965231878dd27b7729e460 [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001/**
2 * @license
3 * Copyright 2016-2017 Leif Olsen. All Rights Reserved.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 * This code is built with Google Material Design Lite,
18 * which is Licensed under the Apache License, Version 2.0
19 */
20
21
22import {jsonStringToObject} from '../utils/json-utils';
23import {
24 IS_UPGRADED,
25} from '../utils/constants';
26
27const JS_FORMAT_FIELD = 'mdlext-js-formatfield';
28const FORMAT_FIELD_COMPONENT = 'MaterialExtFormatfield';
29
30/**
31 * Detect browser locale
32 * @returns {string} the locale
33 * @see http://stackoverflow.com/questions/1043339/javascript-for-detecting-browser-language-preference
34 */
35const browserLanguage = () => {
36 return navigator.languages
37 ? navigator.languages[0]
38 : navigator.language || navigator.userLanguage;
39};
40
41/**
42 * The formatfield formats an input field using language sensitive number formatting.
43 */
44
45class FormatField {
46 static timer = null;
47
48 element_;
49 input_;
50 options_ = {};
51 intlGroupSeparator_;
52 intlDecimalSeparator_;
53
54 constructor(element) {
55 this.element_ = element;
56 this.init();
57 }
58
59 clickHandler = () => {
60 clearTimeout(FormatField.timer);
61 };
62
63 focusInHandler = () => {
64 if(!(this.input.readOnly || this.input.disabled)) {
65 this.input.value = this.unformatInput();
66 //setTimeout(() => this.input.setSelectionRange(0, this.input.value.length), 20);
67 FormatField.timer = setTimeout(() => this.input.select(), 200);
68 }
69 };
70
71 focusOutHandler = () => {
72 clearTimeout(FormatField.timer);
73
74 if(!(this.input.readOnly || this.input.disabled)) {
75 this.formatValue();
76 }
77 };
78
79 get element() {
80 return this.element_;
81 }
82
83 get input() {
84 return this.input_;
85 }
86
87 get options() {
88 return this.options_;
89 }
90
91 stripSeparatorsFromValue() {
92 const doReplace = () => this.input.value
93 .replace(/\s/g, '')
94 .replace(new RegExp(this.options.groupSeparator, 'g'), '')
95 .replace(this.options.decimalSeparator, '.');
96 //.replace(this.intlGroupSeparator_, ''),
97 //.replace(this.intlDecimalSeparator_, '.');
98
99 return this.input.value ? doReplace() : this.input.value;
100 }
101
102 fixSeparators(value) {
103 const doReplace = () => value
104 .replace(new RegExp(this.intlGroupSeparator_, 'g'), this.options.groupSeparator)
105 .replace(this.intlDecimalSeparator_, this.options.decimalSeparator);
106
107 return value ? doReplace() : value;
108 }
109
110 formatValue() {
111 if(this.input.value) {
112 const v = new Intl.NumberFormat(this.options.locales, this.options)
113 .format(this.stripSeparatorsFromValue());
114
115 if('NaN' !== v) {
116 this.input.value = this.fixSeparators(v);
117 }
118 }
119 }
120
121 unformat() {
122 const doReplace = () => this.input.value
123 .replace(/\s/g, '')
124 .replace(new RegExp(this.options.groupSeparator, 'g'), '')
125 .replace(this.options.decimalSeparator, '.');
126
127 return this.input.value ? doReplace() : this.input.value;
128 }
129
130 unformatInput() {
131 const doReplace = () => this.input.value
132 .replace(/\s/g, '')
133 .replace(new RegExp(this.options.groupSeparator, 'g'), '');
134
135 return this.input.value ? doReplace() : this.input.value;
136 }
137
138 removeListeners() {
139 this.input.removeEventListener('click', this.clickHandler);
140 this.input.removeEventListener('focusin', this.focusInHandler);
141 this.input.removeEventListener('focusout', this.focusOutHandler);
142 }
143
144 init() {
145 const addListeners = () => {
146 this.input.addEventListener('click', this.clickHandler);
147 this.input.addEventListener('focusin', this.focusInHandler);
148 this.input.addEventListener('focusout', this.focusOutHandler);
149 };
150
151 const addOptions = () => {
152 const opts = this.element.getAttribute('data-formatfield-options') ||
153 this.input.getAttribute('data-formatfield-options');
154 if(opts) {
155 this.options_ = jsonStringToObject(opts, this.options);
156 }
157 };
158
159 const addLocale = () => {
160 if(!this.options.locales) {
161 this.options.locales = browserLanguage() || 'en-US'; //'nb-NO', //'en-US',
162 }
163 };
164
165 const addGrouping = () => {
166 const s = (1234.5).toLocaleString(this.options.locales, {
167 style: 'decimal',
168 useGrouping: true,
169 minimumFractionDigits: 1,
170 maximumFractionDigits: 1
171 });
172
173 this.intlGroupSeparator_ = s.charAt(1);
174 this.intlDecimalSeparator_ = s.charAt(s.length-2);
175 this.options.groupSeparator = this.options.groupSeparator || this.intlGroupSeparator_;
176 this.options.decimalSeparator = this.options.decimalSeparator || this.intlDecimalSeparator_;
177
178 if(this.options.groupSeparator === this.options.decimalSeparator) {
179 const e = `Error! options.groupSeparator, "${this.options.groupSeparator}" ` +
180 'and options.decimalSeparator, ' +
181 `"${this.options.decimalSeparator}" should not be equal`;
182 throw new Error(e);
183 }
184 };
185
186 this.input_ = this.element.querySelector('input') || this.element;
187
188 addOptions();
189 addLocale();
190 addGrouping();
191 this.formatValue();
192 addListeners();
193 }
194
195 downgrade() {
196 this.removeListeners();
197 }
198
199}
200
201(function() {
202 'use strict';
203
204 /**
205 * @constructor
206 * @param {HTMLElement} element The element that will be upgraded.
207 */
208 const MaterialExtFormatfield = function MaterialExtFormatfield(element) {
209 this.element_ = element;
210 this.formatField_ = null;
211
212 // Initialize instance.
213 this.init();
214 };
215 window['MaterialExtFormatfield'] = MaterialExtFormatfield;
216
217 /**
218 * Initialize component
219 */
220 MaterialExtFormatfield.prototype.init = function() {
221 if (this.element_) {
222 this.element_.classList.add(IS_UPGRADED);
223 this.formatField_ = new FormatField(this.element_);
224
225 // Listen to 'mdl-componentdowngraded' event
226 this.element_.addEventListener('mdl-componentdowngraded', this.mdlDowngrade_.bind(this));
227 }
228 };
229
230 /**
231 * Get options object
232 *
233 * @public
234 *
235 * @returns {Object} the options object
236 */
237 MaterialExtFormatfield.prototype.getOptions = function() {
238 return this.formatField_.options;
239 };
240 MaterialExtFormatfield.prototype['getOptions'] = MaterialExtFormatfield.prototype.getOptions;
241
242
243 /**
244 * A unformatted value is a string value where the locale specific decimal separator
245 * is replaced with a '.' separator and group separators are stripped.
246 * The returned value is suitable for parsing to a JavaScript numerical value.
247 *
248 * @example
249 * input.value = '1 234,5';
250 * inputElement.MaterialExtFormatfield.getUnformattedValue();
251 * // Returns '1234.5'
252 *
253 * @public
254 *
255 * @returns {String} the unformatted value
256 */
257 MaterialExtFormatfield.prototype.getUnformattedValue = function() {
258 return this.formatField_.unformat();
259 };
260 MaterialExtFormatfield.prototype['getUnformattedValue'] = MaterialExtFormatfield.prototype.getUnformattedValue;
261
262 /*
263 * Downgrade component
264 * E.g remove listeners and clean up resources
265 */
266 MaterialExtFormatfield.prototype.mdlDowngrade_ = function() {
267 this.formatField_.downgrade();
268 };
269
270 // The component registers itself. It can assume componentHandler is available
271 // in the global scope.
272 /* eslint no-undef: 0 */
273 componentHandler.register({
274 constructor: MaterialExtFormatfield,
275 classAsString: FORMAT_FIELD_COMPONENT,
276 cssClass: JS_FORMAT_FIELD,
277 widget: true
278 });
279
280})();