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.
- checksums.yaml +4 -4
- data/app/assets/config/decidim_core_manifest.js +1 -0
- data/app/assets/images/decidim/process.svg +10 -0
- data/app/assets/javascripts/decidim.js.es6 +2 -0
- data/app/assets/javascripts/decidim/editor.js.es6 +2 -2
- data/app/assets/javascripts/decidim/filters.js.es6 +1 -1
- data/app/assets/javascripts/decidim/form_filter.component.js.es6 +26 -5
- data/app/assets/javascripts/decidim/form_filter.component.test.js +1 -18
- data/app/assets/javascripts/decidim/foundation.js.es6 +1 -0
- data/app/assets/javascripts/decidim/history.js.es6 +35 -0
- data/app/assets/javascripts/decidim/orders.js.es6 +28 -0
- data/app/assets/stylesheets/decidim/_decidim.scss +1 -0
- data/app/assets/stylesheets/decidim/email.css +21 -3
- data/app/assets/stylesheets/decidim/extras/_leaflet.scss +4 -2
- data/app/assets/stylesheets/decidim/extras/_quill.scss +8 -0
- data/app/assets/stylesheets/decidim/extras/_register_form.scss +9 -0
- data/app/assets/stylesheets/decidim/modules/_buttons.scss +19 -3
- data/app/assets/stylesheets/decidim/utils/_mixins.scss +8 -4
- data/app/commands/decidim/authorize_user.rb +9 -4
- data/app/commands/decidim/create_omniauth_registration.rb +4 -2
- data/app/commands/decidim/create_registration.rb +4 -1
- data/app/commands/decidim/invite_user.rb +3 -1
- data/app/commands/decidim/update_notifications_settings.rb +31 -0
- data/app/constraints/decidim/current_feature.rb +16 -15
- data/app/controllers/concerns/decidim/action_authorization.rb +73 -0
- data/app/controllers/concerns/decidim/feature_settings.rb +2 -5
- data/app/controllers/concerns/decidim/needs_authorization.rb +5 -1
- data/app/controllers/concerns/decidim/user_profile.rb +1 -0
- data/app/controllers/decidim/application_controller.rb +12 -0
- data/app/controllers/decidim/authorizations_controller.rb +10 -12
- data/app/controllers/decidim/cookie_policy_controller.rb +15 -0
- data/app/controllers/decidim/devise/confirmations_controller.rb +4 -0
- data/app/controllers/decidim/devise/invitations_controller.rb +4 -0
- data/app/controllers/decidim/devise/omniauth_registrations_controller.rb +10 -3
- data/app/controllers/decidim/devise/passwords_controller.rb +4 -0
- data/app/controllers/decidim/devise/registrations_controller.rb +21 -0
- data/app/controllers/decidim/devise/sessions_controller.rb +4 -0
- data/app/controllers/decidim/notifications_settings_controller.rb +31 -0
- data/app/controllers/decidim/pages_controller.rb +17 -6
- data/app/controllers/decidim/participatory_processes_controller.rb +5 -5
- data/app/forms/decidim/notifications_settings_form.rb +17 -0
- data/app/forms/decidim/registration_form.rb +2 -1
- data/app/helpers/decidim/action_authorization_helper.rb +82 -0
- data/app/helpers/decidim/application_helper.rb +2 -0
- data/app/helpers/decidim/authorization_form_helper.rb +1 -1
- data/app/helpers/decidim/cookies_helper.rb +11 -0
- data/app/helpers/decidim/decidim_form_helper.rb +18 -0
- data/app/helpers/decidim/language_chooser_helper.rb +18 -0
- data/app/helpers/decidim/layout_helper.rb +0 -5
- data/app/helpers/decidim/localized_locales_helper.rb +1 -1
- data/app/helpers/decidim/meta_tags_helper.rb +104 -0
- data/app/helpers/decidim/orders_helper.rb +28 -0
- data/app/helpers/decidim/resource_helper.rb +46 -6
- data/app/mailers/decidim/application_mailer.rb +2 -2
- data/app/mailers/decidim/decidim_devise_mailer.rb +0 -1
- data/app/mailers/decidim/newsletter_mailer.rb +24 -0
- data/app/models/decidim/abilities/everyone.rb +2 -0
- data/app/models/decidim/feature.rb +30 -0
- data/app/models/decidim/newsletter.rb +24 -0
- data/app/models/decidim/organization.rb +2 -0
- data/app/models/decidim/user.rb +3 -2
- data/app/queries/decidim/highlighted_participatory_processes.rb +10 -0
- data/app/queries/decidim/promoted_participatory_processes.rb +9 -0
- data/app/queries/decidim/public_participatory_processes.rb +10 -0
- data/app/services/decidim/action_authorizer.rb +102 -0
- data/app/services/decidim/resource_search.rb +1 -1
- data/app/uploaders/decidim/official_image_footer_uploader.rb +12 -0
- data/app/uploaders/decidim/official_image_header_uploader.rb +12 -0
- data/app/validators/etiquette_validator.rb +42 -0
- data/app/views/decidim/account/show.html.erb +1 -1
- data/app/views/decidim/authorizations/index.html.erb +4 -4
- data/app/views/decidim/authorizations/new.html.erb +2 -2
- data/app/views/decidim/cookie_policy/accept.js.erb +3 -0
- data/app/views/decidim/devise/confirmations/new.html.erb +3 -1
- data/app/views/decidim/devise/invitations/edit.html.erb +1 -1
- data/app/views/decidim/devise/omniauth_registrations/new.html.erb +1 -1
- data/app/views/decidim/devise/passwords/edit.html.erb +1 -1
- data/app/views/decidim/devise/passwords/new.html.erb +3 -1
- data/app/views/decidim/devise/registrations/edit.html.erb +2 -1
- data/app/views/decidim/devise/registrations/new.html.erb +13 -1
- data/app/views/decidim/devise/sessions/new.html.erb +3 -1
- data/app/views/decidim/newsletter_mailer/newsletter.html.erb +5 -0
- data/app/views/decidim/notifications_settings/show.html.erb +26 -0
- data/app/views/decidim/pages/index.html.erb +34 -0
- data/app/views/decidim/participatory_process_steps/index.html.erb +1 -1
- data/app/views/decidim/participatory_processes/_participatory_process.html.erb +4 -2
- data/app/views/decidim/participatory_processes/_promoted_process.html.erb +1 -1
- data/app/views/decidim/participatory_processes/index.html.erb +3 -3
- data/app/views/decidim/participatory_processes/show.html.erb +30 -9
- data/app/views/decidim/shared/_action_authorization_modal.html.erb +55 -0
- data/app/views/decidim/shared/_login_modal.html.erb +2 -3
- data/app/views/decidim/shared/_orders.html.erb +15 -0
- data/app/views/decidim/shared/_share_modal.html.erb +33 -0
- data/app/views/devise/mailer/organization_admin_invitation_instructions.html.erb +1 -1
- data/app/views/layouts/decidim/_application.html.erb +12 -0
- data/app/views/layouts/decidim/_cookie_warning.html.erb +8 -0
- data/app/views/layouts/decidim/_footer.html.erb +6 -0
- data/app/views/layouts/decidim/_header.html.erb +7 -9
- data/app/views/layouts/decidim/_language_chooser.html.erb +3 -3
- data/app/views/layouts/decidim/_logo.html.erb +1 -1
- data/app/views/layouts/decidim/_mailer_logo.html.erb +31 -0
- data/app/views/layouts/decidim/_main_nav.html.erb +11 -0
- data/app/views/layouts/decidim/_process_header.html.erb +9 -7
- data/app/views/layouts/decidim/_social_media_links.html.erb +39 -0
- data/app/views/layouts/decidim/_social_meta.html.erb +8 -8
- data/app/views/layouts/decidim/mailer.html.erb +110 -62
- data/app/views/layouts/decidim/participatory_process.html.erb +14 -3
- data/app/views/layouts/decidim/user_profile.html.erb +1 -0
- data/app/views/pages/decidim_page.html.erb +5 -0
- data/app/views/pages/home.html.erb +4 -0
- data/app/views/pages/home/_extended.html.erb +2 -2
- data/app/views/pages/home/_footer_sub_hero.html.erb +12 -0
- data/app/views/pages/home/_hero.html.erb +2 -2
- data/app/views/pages/home/_highlighted_processes.html.erb +2 -2
- data/config/i18n-tasks.yml +6 -2
- data/config/initializers/invisible_captcha.rb +10 -0
- data/config/initializers/mail_previews.rb +4 -0
- data/config/locales/ca.yml +129 -23
- data/config/locales/en.yml +98 -13
- data/config/locales/es.yml +128 -22
- data/config/locales/eu.yml +5 -0
- data/config/routes.rb +13 -5
- data/db/migrate/20170131134349_add_action_permissions_to_decidim_features.rb +5 -0
- data/db/migrate/20170202084913_add_comments_and_replies_notifications_to_users.rb +6 -0
- data/db/migrate/20170203150545_add_newsletter_notifications_to_users.rb +5 -0
- data/db/migrate/20170206083118_rename_extra_info_on_processes.rb +12 -0
- data/db/migrate/20170206142116_add_published_at_to_decidim_features.rb +6 -0
- data/db/migrate/20170207091021_add_social_media_handlers_to_organization.rb +8 -0
- data/db/migrate/20170207093048_add_organization_logo_and_url.rb +7 -0
- data/db/migrate/20170213081133_create_decidim_newsletters.rb +15 -0
- data/db/seeds.rb +29 -4
- data/lib/decidim/attributes.rb +6 -0
- data/lib/decidim/attributes/time_with_zone.rb +13 -0
- data/lib/decidim/authorable.rb +2 -2
- data/lib/decidim/core.rb +11 -0
- data/lib/decidim/core/api.rb +11 -0
- data/lib/decidim/core/api/author_interface.rb +11 -0
- data/lib/decidim/core/api/localized_string_type.rb +11 -0
- data/{app/types/decidim → lib/decidim/core/api}/process_step_type.rb +2 -2
- data/{app/types/decidim → lib/decidim/core/api}/process_type.rb +1 -1
- data/{app/types/decidim → lib/decidim/core/api}/session_type.rb +0 -0
- data/lib/decidim/core/api/translated_field_type.rb +42 -0
- data/{app/types/decidim → lib/decidim/core/api}/user_group_type.rb +1 -1
- data/{app/types/decidim → lib/decidim/core/api}/user_type.rb +1 -1
- data/lib/decidim/core/engine.rb +7 -1
- data/lib/decidim/core/test.rb +1 -0
- data/lib/decidim/core/test/factories.rb +41 -8
- data/lib/decidim/core/test/shared_examples/localised_email.rb +24 -0
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/feature_manifest.rb +11 -0
- data/{app/validators → lib/decidim}/feature_validator.rb +1 -3
- data/lib/decidim/features/base_controller.rb +6 -1
- data/lib/decidim/form_builder.rb +166 -1
- data/lib/decidim/has_feature.rb +2 -1
- data/lib/decidim/resourceable.rb +26 -0
- data/vendor/assets/javascripts/morphdom.js +679 -0
- metadata +174 -49
- data/lib/decidim/has_attachment.rb +0 -32
data/lib/decidim/has_feature.rb
CHANGED
@@ -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
|
|
data/lib/decidim/resourceable.rb
CHANGED
@@ -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
|
+
})));
|