1+ /**
2+ * AngularJS module "angular-input-modified".
3+ *
4+ * @version 1.1.3
5+ * @author Slava Fomin II <[email protected] > 6+ * @licence MIT
7+ * @copyright Slava Fomin II, Better Solutions, 2014
8+ */
9+ ( function ( window , angular ) {
10+
11+ 'use strict' ;
12+
13+ var modifiedClassName = 'ng-modified' ;
14+ var notModifiedClassName = 'ng-not-modified' ;
15+
16+ var directiveSpecification = [ '$animate' , inputModifiedDirective ] ;
17+
18+ // Registering AngularJS module.
19+ angular . module ( 'ngInputModified' , [ 'ng' ] )
20+ . directive ( 'input' , directiveSpecification )
21+ . directive ( 'textarea' , directiveSpecification )
22+ . directive ( 'select' , directiveSpecification )
23+ ;
24+
25+ function inputModifiedDirective ( $animate )
26+ {
27+ return {
28+ restrict : 'E' ,
29+ require : [ '?ngModel' , '?^form' ] ,
30+ link : function ( $scope , $element , attrs , controllers ) {
31+
32+ var modelName = attrs . ngModel ;
33+
34+ // Handling controllers.
35+ var ngModel = controllers [ 0 ] ;
36+ var ngForm = controllers [ 1 ] ;
37+
38+ // ngModel is required for this directive to operate.
39+ // ngForm is optional.
40+ if ( null === ngModel ) {
41+ return ;
42+ }
43+
44+ /**
45+ * Decorates element with proper CSS classes.
46+ */
47+ var toggleCssClasses = function ( ) {
48+ $animate . addClass ( $element , ( ngModel . modified ? modifiedClassName : notModifiedClassName ) ) ;
49+ $animate . removeClass ( $element , ( ngModel . modified ? notModifiedClassName : modifiedClassName ) ) ;
50+ } ;
51+
52+ /**
53+ * This handler is called when form element is modified.
54+ *
55+ * @param {string } modelName
56+ * @param {boolean } modified
57+ */
58+ var onElementModified = function ( modelName , modified ) {
59+
60+ // Updating form when one of the inputs is modified.
61+ if ( ngForm ) {
62+ var index = ngForm . modifiedModels . indexOf ( modelName ) ;
63+ var exists = ( - 1 !== index ) ;
64+
65+ if ( modified && ! exists ) {
66+ // Adding model name to the list of modified models.
67+ ngForm . modifiedModels . push ( modelName ) ;
68+ ngForm . modifiedCount ++ ;
69+ } else if ( ! modified && exists ) {
70+ // Removing model name from the list of modified models.
71+ ngForm . modifiedModels . splice ( index , 1 ) ;
72+ ngForm . modifiedCount -- ;
73+ }
74+
75+ // Form is considered modified when it has at least one modified element.
76+ ngForm . modified = ( ngForm . modifiedCount > 0 ) ;
77+ }
78+ } ;
79+
80+ /**
81+ * Sets proper modification state for model controller according to current and master value.
82+ */
83+ var onInputValueChanged = function ( ) {
84+
85+ // If master value is not set.
86+ if ( 'undefined' === typeof ngModel . masterValue ) {
87+
88+ // Initializing the master value.
89+ ngModel . masterValue = ngModel . $modelValue ;
90+
91+ // Initially decorating the element.
92+ toggleCssClasses ( ) ;
93+
94+ } else {
95+
96+ // Comparing current input value with preserved master value
97+ // to determine if it's changed.
98+ var modified = ! valuesEqual ( ngModel . $modelValue , ngModel . masterValue ) ;
99+
100+ // If modified flag is changed.
101+ if ( ngModel . modified !== modified ) {
102+
103+ onElementModified ( modelName , modified ) ;
104+
105+ // Setting new flag.
106+ ngModel . modified = modified ;
107+
108+ // Re-decorating the element.
109+ toggleCssClasses ( ) ;
110+ }
111+ }
112+ } ;
113+
114+ // Saving handle to original $setPristine method.
115+ var originalSetPristine = ngModel . $setPristine ;
116+
117+ /**
118+ * This flag will show if input value was modified.
119+ *
120+ * @type {boolean }
121+ */
122+ ngModel . modified = false ;
123+
124+ /**
125+ * This property contains current master value for this input field.
126+ *
127+ * @type {* }
128+ */
129+ ngModel . masterValue = undefined ;
130+
131+ /**
132+ * Augmentation for original $setPristine method.
133+ */
134+ ngModel . $setPristine = function ( ) {
135+
136+ // Calling original $setPristine method.
137+ originalSetPristine . apply ( this , arguments ) ;
138+
139+ // Updating parameters.
140+ ngModel . masterValue = ngModel . $modelValue ;
141+ ngModel . modified = false ;
142+
143+ if ( ngModel . modified ) {
144+ onElementModified ( modelName , false ) ;
145+ }
146+
147+ // Re-decorating the element.
148+ toggleCssClasses ( ) ;
149+ } ;
150+
151+ /**
152+ * Resets input value to the master.
153+ */
154+ ngModel . reset = function ( ) {
155+ eval ( '$scope.' + modelName + ' = ngModel.masterValue;' ) ;
156+ } ;
157+
158+ // If parent form element is present for this input and
159+ // is not yet initialized.
160+ if ( ngForm && ! isFormInitialized ( ngForm ) ) {
161+ initializeForm ( ngForm ) ;
162+ }
163+
164+ // Watching for model value changes.
165+ $scope . $watch ( modelName , onInputValueChanged ) ;
166+ }
167+ } ;
168+ }
169+
170+ /**
171+ * Returns true if specified parameter is Angular model controller,
172+ * false otherwise.
173+ *
174+ * @param {* } ngModel
175+ *
176+ * @returns {boolean }
177+ */
178+ function isModelController ( ngModel ) {
179+ return (
180+ 'object' === typeof ngModel
181+ && '$modelValue' in ngModel
182+ ) ;
183+ }
184+
185+ /**
186+ * Returns true if specified parameter is initialized model controller,
187+ * false otherwise.
188+ *
189+ * @param {* } ngModel
190+ *
191+ * @returns {boolean }
192+ */
193+ function isModelControllerInitialized ( ngModel ) {
194+ return ( 'modified' in ngModel ) ;
195+ }
196+
197+ /**
198+ * Iterates child model controllers of specified form controller,
199+ * call specified iterator with every model controller.
200+ *
201+ * @param {object } ngForm
202+ * @param {function } iterator
203+ */
204+ function iterateFormElements ( ngForm , iterator ) {
205+ angular . forEach ( ngForm , function ( element ) {
206+ if ( isModelController ( element ) ) {
207+ iterator ( element ) ;
208+ }
209+ } ) ;
210+ }
211+
212+ /**
213+ * Initializes specified form controller.
214+ *
215+ * @param {object } ngForm
216+ */
217+ function initializeForm ( ngForm ) {
218+
219+ ngForm . modified = false ;
220+ ngForm . modifiedCount = 0 ;
221+
222+ // List of modified models.
223+ ngForm . modifiedModels = [ ] ;
224+
225+ /**
226+ * Resets all form inputs to it's master values.
227+ */
228+ ngForm . reset = function ( ) {
229+ iterateFormElements ( ngForm , function ( ngModel ) {
230+ if ( isModelControllerInitialized ( ngModel ) ) {
231+ ngModel . reset ( ) ;
232+ }
233+ } ) ;
234+ } ;
235+ }
236+
237+ /**
238+ * Returns true if specified form controller is initialized, false otherwise.
239+ *
240+ * @param {object } ngForm
241+ *
242+ * @returns {boolean }
243+ */
244+ function isFormInitialized ( ngForm ) {
245+ return ( 'undefined' !== typeof ngForm . modified ) ;
246+ }
247+
248+ /**
249+ * Returns true if specified values are equal, false otherwise.
250+ * Supports date comparison.
251+ *
252+ * @param {* } value1
253+ * @param {* } value2
254+ *
255+ * @returns {boolean }
256+ */
257+ function valuesEqual ( value1 , value2 )
258+ {
259+ value1 = unifyValue ( value1 ) ;
260+ value2 = unifyValue ( value2 ) ;
261+
262+ if ( 'object' === typeof value1 && 'object' === typeof value2 ) {
263+ if ( value1 instanceof Date && value2 instanceof Date ) {
264+ // Comparing two dates.
265+ return ( value1 . getTime ( ) === value2 . getTime ( ) ) ;
266+ } else {
267+ // Comparing two generic objects using strong comparison.
268+ return ( value1 === value2 ) ;
269+ }
270+ }
271+
272+ // In all other cases using weak comparison.
273+ return ( value1 == value2 ) ;
274+ }
275+
276+ /**
277+ * Casting all null-like values to actual null for guaranteed comparison.
278+ *
279+ * @param {* } value
280+ *
281+ * @returns {* }
282+ */
283+ function unifyValue ( value )
284+ {
285+ if ( undefined === value ) {
286+ return null ;
287+ }
288+
289+ if ( '' === value ) {
290+ return null ;
291+ }
292+
293+ return value ;
294+ }
295+
296+ } ) ( window , angular ) ;
0 commit comments