decidim-core 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/decidim_core_manifest.js +1 -0
  3. data/app/assets/images/decidim/process.svg +10 -0
  4. data/app/assets/javascripts/decidim.js.es6 +2 -0
  5. data/app/assets/javascripts/decidim/editor.js.es6 +2 -2
  6. data/app/assets/javascripts/decidim/filters.js.es6 +1 -1
  7. data/app/assets/javascripts/decidim/form_filter.component.js.es6 +26 -5
  8. data/app/assets/javascripts/decidim/form_filter.component.test.js +1 -18
  9. data/app/assets/javascripts/decidim/foundation.js.es6 +1 -0
  10. data/app/assets/javascripts/decidim/history.js.es6 +35 -0
  11. data/app/assets/javascripts/decidim/orders.js.es6 +28 -0
  12. data/app/assets/stylesheets/decidim/_decidim.scss +1 -0
  13. data/app/assets/stylesheets/decidim/email.css +21 -3
  14. data/app/assets/stylesheets/decidim/extras/_leaflet.scss +4 -2
  15. data/app/assets/stylesheets/decidim/extras/_quill.scss +8 -0
  16. data/app/assets/stylesheets/decidim/extras/_register_form.scss +9 -0
  17. data/app/assets/stylesheets/decidim/modules/_buttons.scss +19 -3
  18. data/app/assets/stylesheets/decidim/utils/_mixins.scss +8 -4
  19. data/app/commands/decidim/authorize_user.rb +9 -4
  20. data/app/commands/decidim/create_omniauth_registration.rb +4 -2
  21. data/app/commands/decidim/create_registration.rb +4 -1
  22. data/app/commands/decidim/invite_user.rb +3 -1
  23. data/app/commands/decidim/update_notifications_settings.rb +31 -0
  24. data/app/constraints/decidim/current_feature.rb +16 -15
  25. data/app/controllers/concerns/decidim/action_authorization.rb +73 -0
  26. data/app/controllers/concerns/decidim/feature_settings.rb +2 -5
  27. data/app/controllers/concerns/decidim/needs_authorization.rb +5 -1
  28. data/app/controllers/concerns/decidim/user_profile.rb +1 -0
  29. data/app/controllers/decidim/application_controller.rb +12 -0
  30. data/app/controllers/decidim/authorizations_controller.rb +10 -12
  31. data/app/controllers/decidim/cookie_policy_controller.rb +15 -0
  32. data/app/controllers/decidim/devise/confirmations_controller.rb +4 -0
  33. data/app/controllers/decidim/devise/invitations_controller.rb +4 -0
  34. data/app/controllers/decidim/devise/omniauth_registrations_controller.rb +10 -3
  35. data/app/controllers/decidim/devise/passwords_controller.rb +4 -0
  36. data/app/controllers/decidim/devise/registrations_controller.rb +21 -0
  37. data/app/controllers/decidim/devise/sessions_controller.rb +4 -0
  38. data/app/controllers/decidim/notifications_settings_controller.rb +31 -0
  39. data/app/controllers/decidim/pages_controller.rb +17 -6
  40. data/app/controllers/decidim/participatory_processes_controller.rb +5 -5
  41. data/app/forms/decidim/notifications_settings_form.rb +17 -0
  42. data/app/forms/decidim/registration_form.rb +2 -1
  43. data/app/helpers/decidim/action_authorization_helper.rb +82 -0
  44. data/app/helpers/decidim/application_helper.rb +2 -0
  45. data/app/helpers/decidim/authorization_form_helper.rb +1 -1
  46. data/app/helpers/decidim/cookies_helper.rb +11 -0
  47. data/app/helpers/decidim/decidim_form_helper.rb +18 -0
  48. data/app/helpers/decidim/language_chooser_helper.rb +18 -0
  49. data/app/helpers/decidim/layout_helper.rb +0 -5
  50. data/app/helpers/decidim/localized_locales_helper.rb +1 -1
  51. data/app/helpers/decidim/meta_tags_helper.rb +104 -0
  52. data/app/helpers/decidim/orders_helper.rb +28 -0
  53. data/app/helpers/decidim/resource_helper.rb +46 -6
  54. data/app/mailers/decidim/application_mailer.rb +2 -2
  55. data/app/mailers/decidim/decidim_devise_mailer.rb +0 -1
  56. data/app/mailers/decidim/newsletter_mailer.rb +24 -0
  57. data/app/models/decidim/abilities/everyone.rb +2 -0
  58. data/app/models/decidim/feature.rb +30 -0
  59. data/app/models/decidim/newsletter.rb +24 -0
  60. data/app/models/decidim/organization.rb +2 -0
  61. data/app/models/decidim/user.rb +3 -2
  62. data/app/queries/decidim/highlighted_participatory_processes.rb +10 -0
  63. data/app/queries/decidim/promoted_participatory_processes.rb +9 -0
  64. data/app/queries/decidim/public_participatory_processes.rb +10 -0
  65. data/app/services/decidim/action_authorizer.rb +102 -0
  66. data/app/services/decidim/resource_search.rb +1 -1
  67. data/app/uploaders/decidim/official_image_footer_uploader.rb +12 -0
  68. data/app/uploaders/decidim/official_image_header_uploader.rb +12 -0
  69. data/app/validators/etiquette_validator.rb +42 -0
  70. data/app/views/decidim/account/show.html.erb +1 -1
  71. data/app/views/decidim/authorizations/index.html.erb +4 -4
  72. data/app/views/decidim/authorizations/new.html.erb +2 -2
  73. data/app/views/decidim/cookie_policy/accept.js.erb +3 -0
  74. data/app/views/decidim/devise/confirmations/new.html.erb +3 -1
  75. data/app/views/decidim/devise/invitations/edit.html.erb +1 -1
  76. data/app/views/decidim/devise/omniauth_registrations/new.html.erb +1 -1
  77. data/app/views/decidim/devise/passwords/edit.html.erb +1 -1
  78. data/app/views/decidim/devise/passwords/new.html.erb +3 -1
  79. data/app/views/decidim/devise/registrations/edit.html.erb +2 -1
  80. data/app/views/decidim/devise/registrations/new.html.erb +13 -1
  81. data/app/views/decidim/devise/sessions/new.html.erb +3 -1
  82. data/app/views/decidim/newsletter_mailer/newsletter.html.erb +5 -0
  83. data/app/views/decidim/notifications_settings/show.html.erb +26 -0
  84. data/app/views/decidim/pages/index.html.erb +34 -0
  85. data/app/views/decidim/participatory_process_steps/index.html.erb +1 -1
  86. data/app/views/decidim/participatory_processes/_participatory_process.html.erb +4 -2
  87. data/app/views/decidim/participatory_processes/_promoted_process.html.erb +1 -1
  88. data/app/views/decidim/participatory_processes/index.html.erb +3 -3
  89. data/app/views/decidim/participatory_processes/show.html.erb +30 -9
  90. data/app/views/decidim/shared/_action_authorization_modal.html.erb +55 -0
  91. data/app/views/decidim/shared/_login_modal.html.erb +2 -3
  92. data/app/views/decidim/shared/_orders.html.erb +15 -0
  93. data/app/views/decidim/shared/_share_modal.html.erb +33 -0
  94. data/app/views/devise/mailer/organization_admin_invitation_instructions.html.erb +1 -1
  95. data/app/views/layouts/decidim/_application.html.erb +12 -0
  96. data/app/views/layouts/decidim/_cookie_warning.html.erb +8 -0
  97. data/app/views/layouts/decidim/_footer.html.erb +6 -0
  98. data/app/views/layouts/decidim/_header.html.erb +7 -9
  99. data/app/views/layouts/decidim/_language_chooser.html.erb +3 -3
  100. data/app/views/layouts/decidim/_logo.html.erb +1 -1
  101. data/app/views/layouts/decidim/_mailer_logo.html.erb +31 -0
  102. data/app/views/layouts/decidim/_main_nav.html.erb +11 -0
  103. data/app/views/layouts/decidim/_process_header.html.erb +9 -7
  104. data/app/views/layouts/decidim/_social_media_links.html.erb +39 -0
  105. data/app/views/layouts/decidim/_social_meta.html.erb +8 -8
  106. data/app/views/layouts/decidim/mailer.html.erb +110 -62
  107. data/app/views/layouts/decidim/participatory_process.html.erb +14 -3
  108. data/app/views/layouts/decidim/user_profile.html.erb +1 -0
  109. data/app/views/pages/decidim_page.html.erb +5 -0
  110. data/app/views/pages/home.html.erb +4 -0
  111. data/app/views/pages/home/_extended.html.erb +2 -2
  112. data/app/views/pages/home/_footer_sub_hero.html.erb +12 -0
  113. data/app/views/pages/home/_hero.html.erb +2 -2
  114. data/app/views/pages/home/_highlighted_processes.html.erb +2 -2
  115. data/config/i18n-tasks.yml +6 -2
  116. data/config/initializers/invisible_captcha.rb +10 -0
  117. data/config/initializers/mail_previews.rb +4 -0
  118. data/config/locales/ca.yml +129 -23
  119. data/config/locales/en.yml +98 -13
  120. data/config/locales/es.yml +128 -22
  121. data/config/locales/eu.yml +5 -0
  122. data/config/routes.rb +13 -5
  123. data/db/migrate/20170131134349_add_action_permissions_to_decidim_features.rb +5 -0
  124. data/db/migrate/20170202084913_add_comments_and_replies_notifications_to_users.rb +6 -0
  125. data/db/migrate/20170203150545_add_newsletter_notifications_to_users.rb +5 -0
  126. data/db/migrate/20170206083118_rename_extra_info_on_processes.rb +12 -0
  127. data/db/migrate/20170206142116_add_published_at_to_decidim_features.rb +6 -0
  128. data/db/migrate/20170207091021_add_social_media_handlers_to_organization.rb +8 -0
  129. data/db/migrate/20170207093048_add_organization_logo_and_url.rb +7 -0
  130. data/db/migrate/20170213081133_create_decidim_newsletters.rb +15 -0
  131. data/db/seeds.rb +29 -4
  132. data/lib/decidim/attributes.rb +6 -0
  133. data/lib/decidim/attributes/time_with_zone.rb +13 -0
  134. data/lib/decidim/authorable.rb +2 -2
  135. data/lib/decidim/core.rb +11 -0
  136. data/lib/decidim/core/api.rb +11 -0
  137. data/lib/decidim/core/api/author_interface.rb +11 -0
  138. data/lib/decidim/core/api/localized_string_type.rb +11 -0
  139. data/{app/types/decidim → lib/decidim/core/api}/process_step_type.rb +2 -2
  140. data/{app/types/decidim → lib/decidim/core/api}/process_type.rb +1 -1
  141. data/{app/types/decidim → lib/decidim/core/api}/session_type.rb +0 -0
  142. data/lib/decidim/core/api/translated_field_type.rb +42 -0
  143. data/{app/types/decidim → lib/decidim/core/api}/user_group_type.rb +1 -1
  144. data/{app/types/decidim → lib/decidim/core/api}/user_type.rb +1 -1
  145. data/lib/decidim/core/engine.rb +7 -1
  146. data/lib/decidim/core/test.rb +1 -0
  147. data/lib/decidim/core/test/factories.rb +41 -8
  148. data/lib/decidim/core/test/shared_examples/localised_email.rb +24 -0
  149. data/lib/decidim/core/version.rb +1 -1
  150. data/lib/decidim/feature_manifest.rb +11 -0
  151. data/{app/validators → lib/decidim}/feature_validator.rb +1 -3
  152. data/lib/decidim/features/base_controller.rb +6 -1
  153. data/lib/decidim/form_builder.rb +166 -1
  154. data/lib/decidim/has_feature.rb +2 -1
  155. data/lib/decidim/resourceable.rb +26 -0
  156. data/vendor/assets/javascripts/morphdom.js +679 -0
  157. metadata +174 -49
  158. data/lib/decidim/has_attachment.rb +0 -32
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "active_support/concern"
3
+ require "decidim/feature_validator"
3
4
 
4
5
  module Decidim
5
6
  # A concern with the features needed when you want a model to have a feature.
@@ -7,7 +8,7 @@ module Decidim
7
8
  extend ActiveSupport::Concern
8
9
 
9
10
  included do
10
- belongs_to :feature, foreign_key: "decidim_feature_id", class_name: Decidim::Feature
11
+ belongs_to :feature, foreign_key: "decidim_feature_id", class_name: "Decidim::Feature"
11
12
  has_one :organization, through: :feature
12
13
  end
13
14
 
@@ -74,6 +74,32 @@ module Decidim
74
74
  end
75
75
 
76
76
  class_methods do
77
+ # Finds the name of the classes that have been linked to this model for the given
78
+ # `feature`.
79
+ #
80
+ # feature - a Decidim::Feature instance where the links will be scoped to.
81
+ #
82
+ # Returns an Array of Strings.
83
+ def linked_classes_for(feature)
84
+ scope = where(feature: feature)
85
+
86
+ from = scope
87
+ .joins(:resource_links_from)
88
+ .where(decidim_resource_links: { from_type: name })
89
+
90
+ to = scope
91
+ .joins(:resource_links_to)
92
+ .where(decidim_resource_links: { to_type: name })
93
+
94
+ ResourceLink
95
+ .where(from: from)
96
+ .or(ResourceLink.where(to: to))
97
+ .pluck(:from_type, :to_type)
98
+ .flatten
99
+ .uniq
100
+ .reject { |k| k == name }
101
+ end
102
+
77
103
  # Finds the resource manifest for the model.
78
104
  #
79
105
  # Returns a Decidim::ResourceManifest
@@ -0,0 +1,679 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
+ typeof define === 'function' && define.amd ? define(factory) :
4
+ (global.morphdom = factory());
5
+ }(this, (function () { 'use strict';
6
+
7
+ var range; // Create a range object for efficently rendering strings to elements.
8
+ var NS_XHTML = 'http://www.w3.org/1999/xhtml';
9
+
10
+ var doc = typeof document === 'undefined' ? undefined : document;
11
+
12
+ var testEl = doc ?
13
+ doc.body || doc.createElement('div') :
14
+ {};
15
+
16
+ // Fixes <https://github.com/patrick-steele-idem/morphdom/issues/32>
17
+ // (IE7+ support) <=IE7 does not support el.hasAttribute(name)
18
+ var actualHasAttributeNS;
19
+
20
+ if (testEl.hasAttributeNS) {
21
+ actualHasAttributeNS = function(el, namespaceURI, name) {
22
+ return el.hasAttributeNS(namespaceURI, name);
23
+ };
24
+ } else if (testEl.hasAttribute) {
25
+ actualHasAttributeNS = function(el, namespaceURI, name) {
26
+ return el.hasAttribute(name);
27
+ };
28
+ } else {
29
+ actualHasAttributeNS = function(el, namespaceURI, name) {
30
+ return el.getAttributeNode(namespaceURI, name) != null;
31
+ };
32
+ }
33
+
34
+ var hasAttributeNS = actualHasAttributeNS;
35
+
36
+
37
+ function toElement(str) {
38
+ if (!range && doc.createRange) {
39
+ range = doc.createRange();
40
+ range.selectNode(doc.body);
41
+ }
42
+
43
+ var fragment;
44
+ if (range && range.createContextualFragment) {
45
+ fragment = range.createContextualFragment(str);
46
+ } else {
47
+ fragment = doc.createElement('body');
48
+ fragment.innerHTML = str;
49
+ }
50
+ return fragment.childNodes[0];
51
+ }
52
+
53
+ /**
54
+ * Returns true if two node's names are the same.
55
+ *
56
+ * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
57
+ * nodeName and different namespace URIs.
58
+ *
59
+ * @param {Element} a
60
+ * @param {Element} b The target element
61
+ * @return {boolean}
62
+ */
63
+ function compareNodeNames(fromEl, toEl) {
64
+ var fromNodeName = fromEl.nodeName;
65
+ var toNodeName = toEl.nodeName;
66
+
67
+ if (fromNodeName === toNodeName) {
68
+ return true;
69
+ }
70
+
71
+ if (toEl.actualize &&
72
+ fromNodeName.charCodeAt(0) < 91 && /* from tag name is upper case */
73
+ toNodeName.charCodeAt(0) > 90 /* target tag name is lower case */) {
74
+ // If the target element is a virtual DOM node then we may need to normalize the tag name
75
+ // before comparing. Normal HTML elements that are in the "http://www.w3.org/1999/xhtml"
76
+ // are converted to upper case
77
+ return fromNodeName === toNodeName.toUpperCase();
78
+ } else {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Create an element, optionally with a known namespace URI.
85
+ *
86
+ * @param {string} name the element name, e.g. 'div' or 'svg'
87
+ * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
88
+ * its `xmlns` attribute or its inferred namespace.
89
+ *
90
+ * @return {Element}
91
+ */
92
+ function createElementNS(name, namespaceURI) {
93
+ return !namespaceURI || namespaceURI === NS_XHTML ?
94
+ doc.createElement(name) :
95
+ doc.createElementNS(namespaceURI, name);
96
+ }
97
+
98
+ /**
99
+ * Copies the children of one DOM element to another DOM element
100
+ */
101
+ function moveChildren(fromEl, toEl) {
102
+ var curChild = fromEl.firstChild;
103
+ while (curChild) {
104
+ var nextChild = curChild.nextSibling;
105
+ toEl.appendChild(curChild);
106
+ curChild = nextChild;
107
+ }
108
+ return toEl;
109
+ }
110
+
111
+ function morphAttrs(fromNode, toNode) {
112
+ var attrs = toNode.attributes;
113
+ var i;
114
+ var attr;
115
+ var attrName;
116
+ var attrNamespaceURI;
117
+ var attrValue;
118
+ var fromValue;
119
+
120
+ for (i = attrs.length - 1; i >= 0; --i) {
121
+ attr = attrs[i];
122
+ attrName = attr.name;
123
+ attrNamespaceURI = attr.namespaceURI;
124
+ attrValue = attr.value;
125
+
126
+ if (attrNamespaceURI) {
127
+ attrName = attr.localName || attrName;
128
+ fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
129
+
130
+ if (fromValue !== attrValue) {
131
+ fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
132
+ }
133
+ } else {
134
+ fromValue = fromNode.getAttribute(attrName);
135
+
136
+ if (fromValue !== attrValue) {
137
+ fromNode.setAttribute(attrName, attrValue);
138
+ }
139
+ }
140
+ }
141
+
142
+ // Remove any extra attributes found on the original DOM element that
143
+ // weren't found on the target element.
144
+ attrs = fromNode.attributes;
145
+
146
+ for (i = attrs.length - 1; i >= 0; --i) {
147
+ attr = attrs[i];
148
+ if (attr.specified !== false) {
149
+ attrName = attr.name;
150
+ attrNamespaceURI = attr.namespaceURI;
151
+
152
+ if (attrNamespaceURI) {
153
+ attrName = attr.localName || attrName;
154
+
155
+ if (!hasAttributeNS(toNode, attrNamespaceURI, attrName)) {
156
+ fromNode.removeAttributeNS(attrNamespaceURI, attrName);
157
+ }
158
+ } else {
159
+ if (!hasAttributeNS(toNode, null, attrName)) {
160
+ fromNode.removeAttribute(attrName);
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ function syncBooleanAttrProp(fromEl, toEl, name) {
168
+ if (fromEl[name] !== toEl[name]) {
169
+ fromEl[name] = toEl[name];
170
+ if (fromEl[name]) {
171
+ fromEl.setAttribute(name, '');
172
+ } else {
173
+ fromEl.removeAttribute(name, '');
174
+ }
175
+ }
176
+ }
177
+
178
+ var specialElHandlers = {
179
+ /**
180
+ * Needed for IE. Apparently IE doesn't think that "selected" is an
181
+ * attribute when reading over the attributes using selectEl.attributes
182
+ */
183
+ OPTION: function(fromEl, toEl) {
184
+ syncBooleanAttrProp(fromEl, toEl, 'selected');
185
+ },
186
+ /**
187
+ * The "value" attribute is special for the <input> element since it sets
188
+ * the initial value. Changing the "value" attribute without changing the
189
+ * "value" property will have no effect since it is only used to the set the
190
+ * initial value. Similar for the "checked" attribute, and "disabled".
191
+ */
192
+ INPUT: function(fromEl, toEl) {
193
+ syncBooleanAttrProp(fromEl, toEl, 'checked');
194
+ syncBooleanAttrProp(fromEl, toEl, 'disabled');
195
+
196
+ if (fromEl.value !== toEl.value) {
197
+ fromEl.value = toEl.value;
198
+ }
199
+
200
+ if (!hasAttributeNS(toEl, null, 'value')) {
201
+ fromEl.removeAttribute('value');
202
+ }
203
+ },
204
+
205
+ TEXTAREA: function(fromEl, toEl) {
206
+ var newValue = toEl.value;
207
+ if (fromEl.value !== newValue) {
208
+ fromEl.value = newValue;
209
+ }
210
+
211
+ if (fromEl.firstChild) {
212
+ // Needed for IE. Apparently IE sets the placeholder as the
213
+ // node value and vise versa. This ignores an empty update.
214
+ if (newValue === '' && fromEl.firstChild.nodeValue === fromEl.placeholder) {
215
+ return;
216
+ }
217
+
218
+ fromEl.firstChild.nodeValue = newValue;
219
+ }
220
+ },
221
+ SELECT: function(fromEl, toEl) {
222
+ if (!hasAttributeNS(toEl, null, 'multiple')) {
223
+ var selectedIndex = -1;
224
+ var i = 0;
225
+ var curChild = toEl.firstChild;
226
+ while(curChild) {
227
+ var nodeName = curChild.nodeName;
228
+ if (nodeName && nodeName.toUpperCase() === 'OPTION') {
229
+ if (hasAttributeNS(curChild, null, 'selected')) {
230
+ selectedIndex = i;
231
+ break;
232
+ }
233
+ i++;
234
+ }
235
+ curChild = curChild.nextSibling;
236
+ }
237
+
238
+ fromEl.selectedIndex = i;
239
+ }
240
+ }
241
+ };
242
+
243
+ var ELEMENT_NODE = 1;
244
+ var TEXT_NODE = 3;
245
+ var COMMENT_NODE = 8;
246
+
247
+ function noop() {}
248
+
249
+ function defaultGetNodeKey(node) {
250
+ return node.id;
251
+ }
252
+
253
+ function morphdomFactory(morphAttrs) {
254
+
255
+ return function morphdom(fromNode, toNode, options) {
256
+ if (!options) {
257
+ options = {};
258
+ }
259
+
260
+ if (typeof toNode === 'string') {
261
+ if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML') {
262
+ var toNodeHtml = toNode;
263
+ toNode = doc.createElement('html');
264
+ toNode.innerHTML = toNodeHtml;
265
+ } else {
266
+ toNode = toElement(toNode);
267
+ }
268
+ }
269
+
270
+ var getNodeKey = options.getNodeKey || defaultGetNodeKey;
271
+ var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
272
+ var onNodeAdded = options.onNodeAdded || noop;
273
+ var onBeforeElUpdated = options.onBeforeElUpdated || noop;
274
+ var onElUpdated = options.onElUpdated || noop;
275
+ var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
276
+ var onNodeDiscarded = options.onNodeDiscarded || noop;
277
+ var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
278
+ var childrenOnly = options.childrenOnly === true;
279
+
280
+ // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
281
+ var fromNodesLookup = {};
282
+ var keyedRemovalList;
283
+
284
+ function addKeyedRemoval(key) {
285
+ if (keyedRemovalList) {
286
+ keyedRemovalList.push(key);
287
+ } else {
288
+ keyedRemovalList = [key];
289
+ }
290
+ }
291
+
292
+ function walkDiscardedChildNodes(node, skipKeyedNodes) {
293
+ if (node.nodeType === ELEMENT_NODE) {
294
+ var curChild = node.firstChild;
295
+ while (curChild) {
296
+
297
+ var key = undefined;
298
+
299
+ if (skipKeyedNodes && (key = getNodeKey(curChild))) {
300
+ // If we are skipping keyed nodes then we add the key
301
+ // to a list so that it can be handled at the very end.
302
+ addKeyedRemoval(key);
303
+ } else {
304
+ // Only report the node as discarded if it is not keyed. We do this because
305
+ // at the end we loop through all keyed elements that were unmatched
306
+ // and then discard them in one final pass.
307
+ onNodeDiscarded(curChild);
308
+ if (curChild.firstChild) {
309
+ walkDiscardedChildNodes(curChild, skipKeyedNodes);
310
+ }
311
+ }
312
+
313
+ curChild = curChild.nextSibling;
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Removes a DOM node out of the original DOM
320
+ *
321
+ * @param {Node} node The node to remove
322
+ * @param {Node} parentNode The nodes parent
323
+ * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
324
+ * @return {undefined}
325
+ */
326
+ function removeNode(node, parentNode, skipKeyedNodes) {
327
+ if (onBeforeNodeDiscarded(node) === false) {
328
+ return;
329
+ }
330
+
331
+ if (parentNode) {
332
+ parentNode.removeChild(node);
333
+ }
334
+
335
+ onNodeDiscarded(node);
336
+ walkDiscardedChildNodes(node, skipKeyedNodes);
337
+ }
338
+
339
+ // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
340
+ // function indexTree(root) {
341
+ // var treeWalker = document.createTreeWalker(
342
+ // root,
343
+ // NodeFilter.SHOW_ELEMENT);
344
+ //
345
+ // var el;
346
+ // while((el = treeWalker.nextNode())) {
347
+ // var key = getNodeKey(el);
348
+ // if (key) {
349
+ // fromNodesLookup[key] = el;
350
+ // }
351
+ // }
352
+ // }
353
+
354
+ // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
355
+ //
356
+ // function indexTree(node) {
357
+ // var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
358
+ // var el;
359
+ // while((el = nodeIterator.nextNode())) {
360
+ // var key = getNodeKey(el);
361
+ // if (key) {
362
+ // fromNodesLookup[key] = el;
363
+ // }
364
+ // }
365
+ // }
366
+
367
+ function indexTree(node) {
368
+ if (node.nodeType === ELEMENT_NODE) {
369
+ var curChild = node.firstChild;
370
+ while (curChild) {
371
+ var key = getNodeKey(curChild);
372
+ if (key) {
373
+ fromNodesLookup[key] = curChild;
374
+ }
375
+
376
+ // Walk recursively
377
+ indexTree(curChild);
378
+
379
+ curChild = curChild.nextSibling;
380
+ }
381
+ }
382
+ }
383
+
384
+ indexTree(fromNode);
385
+
386
+ function handleNodeAdded(el) {
387
+ onNodeAdded(el);
388
+
389
+ var curChild = el.firstChild;
390
+ while (curChild) {
391
+ var nextSibling = curChild.nextSibling;
392
+
393
+ var key = getNodeKey(curChild);
394
+ if (key) {
395
+ var unmatchedFromEl = fromNodesLookup[key];
396
+ if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
397
+ curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
398
+ morphEl(unmatchedFromEl, curChild);
399
+ }
400
+ }
401
+
402
+ handleNodeAdded(curChild);
403
+ curChild = nextSibling;
404
+ }
405
+ }
406
+
407
+ function morphEl(fromEl, toEl, childrenOnly) {
408
+ var toElKey = getNodeKey(toEl);
409
+ var curFromNodeKey;
410
+
411
+ if (toElKey) {
412
+ // If an element with an ID is being morphed then it is will be in the final
413
+ // DOM so clear it out of the saved elements collection
414
+ delete fromNodesLookup[toElKey];
415
+ }
416
+
417
+ if (toNode.isSameNode && toNode.isSameNode(fromNode)) {
418
+ return;
419
+ }
420
+
421
+ if (!childrenOnly) {
422
+ if (onBeforeElUpdated(fromEl, toEl) === false) {
423
+ return;
424
+ }
425
+
426
+ morphAttrs(fromEl, toEl);
427
+ onElUpdated(fromEl);
428
+
429
+ if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
430
+ return;
431
+ }
432
+ }
433
+
434
+ if (fromEl.nodeName !== 'TEXTAREA') {
435
+ var curToNodeChild = toEl.firstChild;
436
+ var curFromNodeChild = fromEl.firstChild;
437
+ var curToNodeKey;
438
+
439
+ var fromNextSibling;
440
+ var toNextSibling;
441
+ var matchingFromEl;
442
+
443
+ outer: while (curToNodeChild) {
444
+ toNextSibling = curToNodeChild.nextSibling;
445
+ curToNodeKey = getNodeKey(curToNodeChild);
446
+
447
+ while (curFromNodeChild) {
448
+ fromNextSibling = curFromNodeChild.nextSibling;
449
+
450
+ if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
451
+ curToNodeChild = toNextSibling;
452
+ curFromNodeChild = fromNextSibling;
453
+ continue outer;
454
+ }
455
+
456
+ curFromNodeKey = getNodeKey(curFromNodeChild);
457
+
458
+ var curFromNodeType = curFromNodeChild.nodeType;
459
+
460
+ var isCompatible = undefined;
461
+
462
+ if (curFromNodeType === curToNodeChild.nodeType) {
463
+ if (curFromNodeType === ELEMENT_NODE) {
464
+ // Both nodes being compared are Element nodes
465
+
466
+ if (curToNodeKey) {
467
+ // The target node has a key so we want to match it up with the correct element
468
+ // in the original DOM tree
469
+ if (curToNodeKey !== curFromNodeKey) {
470
+ // The current element in the original DOM tree does not have a matching key so
471
+ // let's check our lookup to see if there is a matching element in the original
472
+ // DOM tree
473
+ if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
474
+ if (curFromNodeChild.nextSibling === matchingFromEl) {
475
+ // Special case for single element removals. To avoid removing the original
476
+ // DOM node out of the tree (since that can break CSS transitions, etc.),
477
+ // we will instead discard the current node and wait until the next
478
+ // iteration to properly match up the keyed target element with its matching
479
+ // element in the original tree
480
+ isCompatible = false;
481
+ } else {
482
+ // We found a matching keyed element somewhere in the original DOM tree.
483
+ // Let's moving the original DOM node into the current position and morph
484
+ // it.
485
+
486
+ // NOTE: We use insertBefore instead of replaceChild because we want to go through
487
+ // the `removeNode()` function for the node that is being discarded so that
488
+ // all lifecycle hooks are correctly invoked
489
+ fromEl.insertBefore(matchingFromEl, curFromNodeChild);
490
+
491
+ fromNextSibling = curFromNodeChild.nextSibling;
492
+
493
+ if (curFromNodeKey) {
494
+ // Since the node is keyed it might be matched up later so we defer
495
+ // the actual removal to later
496
+ addKeyedRemoval(curFromNodeKey);
497
+ } else {
498
+ // NOTE: we skip nested keyed nodes from being removed since there is
499
+ // still a chance they will be matched up later
500
+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
501
+ }
502
+
503
+ curFromNodeChild = matchingFromEl;
504
+ }
505
+ } else {
506
+ // The nodes are not compatible since the "to" node has a key and there
507
+ // is no matching keyed node in the source tree
508
+ isCompatible = false;
509
+ }
510
+ }
511
+ } else if (curFromNodeKey) {
512
+ // The original has a key
513
+ isCompatible = false;
514
+ }
515
+
516
+ isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
517
+ if (isCompatible) {
518
+ // We found compatible DOM elements so transform
519
+ // the current "from" node to match the current
520
+ // target DOM node.
521
+ morphEl(curFromNodeChild, curToNodeChild);
522
+ }
523
+
524
+ } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
525
+ // Both nodes being compared are Text or Comment nodes
526
+ isCompatible = true;
527
+ // Simply update nodeValue on the original node to
528
+ // change the text value
529
+ curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
530
+ }
531
+ }
532
+
533
+ if (isCompatible) {
534
+ // Advance both the "to" child and the "from" child since we found a match
535
+ curToNodeChild = toNextSibling;
536
+ curFromNodeChild = fromNextSibling;
537
+ continue outer;
538
+ }
539
+
540
+ // No compatible match so remove the old node from the DOM and continue trying to find a
541
+ // match in the original DOM. However, we only do this if the from node is not keyed
542
+ // since it is possible that a keyed node might match up with a node somewhere else in the
543
+ // target tree and we don't want to discard it just yet since it still might find a
544
+ // home in the final DOM tree. After everything is done we will remove any keyed nodes
545
+ // that didn't find a home
546
+ if (curFromNodeKey) {
547
+ // Since the node is keyed it might be matched up later so we defer
548
+ // the actual removal to later
549
+ addKeyedRemoval(curFromNodeKey);
550
+ } else {
551
+ // NOTE: we skip nested keyed nodes from being removed since there is
552
+ // still a chance they will be matched up later
553
+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
554
+ }
555
+
556
+ curFromNodeChild = fromNextSibling;
557
+ }
558
+
559
+ // If we got this far then we did not find a candidate match for
560
+ // our "to node" and we exhausted all of the children "from"
561
+ // nodes. Therefore, we will just append the current "to" node
562
+ // to the end
563
+ if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
564
+ fromEl.appendChild(matchingFromEl);
565
+ morphEl(matchingFromEl, curToNodeChild);
566
+ } else {
567
+ var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
568
+ if (onBeforeNodeAddedResult !== false) {
569
+ if (onBeforeNodeAddedResult) {
570
+ curToNodeChild = onBeforeNodeAddedResult;
571
+ }
572
+
573
+ if (curToNodeChild.actualize) {
574
+ curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
575
+ }
576
+ fromEl.appendChild(curToNodeChild);
577
+ handleNodeAdded(curToNodeChild);
578
+ }
579
+ }
580
+
581
+ curToNodeChild = toNextSibling;
582
+ curFromNodeChild = fromNextSibling;
583
+ }
584
+
585
+ // We have processed all of the "to nodes". If curFromNodeChild is
586
+ // non-null then we still have some from nodes left over that need
587
+ // to be removed
588
+ while (curFromNodeChild) {
589
+ fromNextSibling = curFromNodeChild.nextSibling;
590
+ if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
591
+ // Since the node is keyed it might be matched up later so we defer
592
+ // the actual removal to later
593
+ addKeyedRemoval(curFromNodeKey);
594
+ } else {
595
+ // NOTE: we skip nested keyed nodes from being removed since there is
596
+ // still a chance they will be matched up later
597
+ removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
598
+ }
599
+ curFromNodeChild = fromNextSibling;
600
+ }
601
+ }
602
+
603
+ var specialElHandler = specialElHandlers[fromEl.nodeName];
604
+ if (specialElHandler) {
605
+ specialElHandler(fromEl, toEl);
606
+ }
607
+ } // END: morphEl(...)
608
+
609
+ var morphedNode = fromNode;
610
+ var morphedNodeType = morphedNode.nodeType;
611
+ var toNodeType = toNode.nodeType;
612
+
613
+ if (!childrenOnly) {
614
+ // Handle the case where we are given two DOM nodes that are not
615
+ // compatible (e.g. <div> --> <span> or <div> --> TEXT)
616
+ if (morphedNodeType === ELEMENT_NODE) {
617
+ if (toNodeType === ELEMENT_NODE) {
618
+ if (!compareNodeNames(fromNode, toNode)) {
619
+ onNodeDiscarded(fromNode);
620
+ morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
621
+ }
622
+ } else {
623
+ // Going from an element node to a text node
624
+ morphedNode = toNode;
625
+ }
626
+ } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
627
+ if (toNodeType === morphedNodeType) {
628
+ morphedNode.nodeValue = toNode.nodeValue;
629
+ return morphedNode;
630
+ } else {
631
+ // Text node to something else
632
+ morphedNode = toNode;
633
+ }
634
+ }
635
+ }
636
+
637
+ if (morphedNode === toNode) {
638
+ // The "to node" was not compatible with the "from node" so we had to
639
+ // toss out the "from node" and use the "to node"
640
+ onNodeDiscarded(fromNode);
641
+ } else {
642
+ morphEl(morphedNode, toNode, childrenOnly);
643
+
644
+ // We now need to loop over any keyed nodes that might need to be
645
+ // removed. We only do the removal if we know that the keyed node
646
+ // never found a match. When a keyed node is matched up we remove
647
+ // it out of fromNodesLookup and we use fromNodesLookup to determine
648
+ // if a keyed node has been matched up or not
649
+ if (keyedRemovalList) {
650
+ for (var i=0, len=keyedRemovalList.length; i<len; i++) {
651
+ var elToRemove = fromNodesLookup[keyedRemovalList[i]];
652
+ if (elToRemove) {
653
+ removeNode(elToRemove, elToRemove.parentNode, false);
654
+ }
655
+ }
656
+ }
657
+ }
658
+
659
+ if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
660
+ if (morphedNode.actualize) {
661
+ morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
662
+ }
663
+ // If we had to swap out the from node with a new node because the old
664
+ // node was not compatible with the target node then we need to
665
+ // replace the old DOM node in the original DOM tree. This is only
666
+ // possible if the original DOM node was part of a DOM tree which
667
+ // we know is the case if it has a parent node.
668
+ fromNode.parentNode.replaceChild(morphedNode, fromNode);
669
+ }
670
+
671
+ return morphedNode;
672
+ };
673
+ }
674
+
675
+ var morphdom = morphdomFactory(morphAttrs);
676
+
677
+ return morphdom;
678
+
679
+ })));