decidim-assemblies 0.24.3 → 0.25.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/assemblies/assembly_m_cell.rb +1 -1
  3. data/app/cells/decidim/assemblies/assembly_member/show.erb +1 -1
  4. data/app/cells/decidim/assemblies/content_blocks/highlighted_assemblies/show.erb +1 -1
  5. data/app/commands/decidim/assemblies/admin/copy_assembly.rb +11 -3
  6. data/app/commands/decidim/assemblies/admin/create_assembly.rb +2 -1
  7. data/app/commands/decidim/assemblies/admin/create_assembly_member.rb +5 -1
  8. data/app/commands/decidim/assemblies/admin/update_assembly.rb +7 -11
  9. data/app/controllers/decidim/assemblies/admin/assemblies_controller.rb +1 -5
  10. data/app/forms/decidim/assemblies/admin/assembly_form.rb +3 -2
  11. data/app/forms/decidim/assemblies/admin/assembly_import_form.rb +1 -0
  12. data/app/forms/decidim/assemblies/admin/assembly_member_form.rb +1 -1
  13. data/app/helpers/decidim/assemblies/admin/assemblies_admin_menu_helper.rb +0 -5
  14. data/app/models/decidim/assembly.rb +5 -5
  15. data/app/models/decidim/assembly_member.rb +1 -1
  16. data/app/packs/entrypoints/decidim_assemblies.js +5 -0
  17. data/app/packs/entrypoints/decidim_assemblies_admin.js +3 -0
  18. data/app/{assets/images/decidim/assemblies/assembly.svg → packs/images/decidim/assemblies/decidim_assemblies.svg} +0 -0
  19. data/app/packs/src/decidim/assemblies/admin/assemblies.js +63 -0
  20. data/app/{assets/javascripts/decidim/assemblies/admin/assembly_members.js.es6 → packs/src/decidim/assemblies/admin/assembly_members.js} +3 -3
  21. data/app/packs/src/decidim/assemblies/assemblies.js +14 -0
  22. data/app/packs/src/decidim/assemblies/orgchart.js +695 -0
  23. data/app/presenters/decidim/assemblies/assembly_presenter.rb +2 -12
  24. data/app/presenters/decidim/assemblies/assembly_stats_presenter.rb +3 -13
  25. data/app/queries/decidim/assemblies/admin/admin_users.rb +24 -11
  26. data/app/serializers/decidim/assemblies/assembly_importer.rb +3 -2
  27. data/app/serializers/decidim/assemblies/assembly_serializer.rb +4 -2
  28. data/app/views/decidim/assemblies/admin/assemblies/_form.html.erb +6 -3
  29. data/app/views/decidim/assemblies/admin/assembly_copies/_form.html.erb +1 -1
  30. data/app/views/decidim/assemblies/admin/assembly_imports/_form.html.erb +1 -1
  31. data/app/views/decidim/assemblies/admin/assembly_members/_form.html.erb +2 -2
  32. data/app/views/decidim/assemblies/assemblies/_promoted_assembly.html.erb +1 -1
  33. data/app/views/decidim/assemblies/assemblies/index.html.erb +2 -2
  34. data/app/views/decidim/assemblies/assemblies/show.html.erb +6 -4
  35. data/app/views/layouts/decidim/_assembly_header.html.erb +1 -1
  36. data/app/views/layouts/decidim/admin/assemblies.html.erb +1 -1
  37. data/app/views/layouts/decidim/admin/assembly.html.erb +4 -73
  38. data/app/views/layouts/decidim/assembly.html.erb +2 -2
  39. data/config/assets.rb +9 -0
  40. data/config/locales/ar.yml +0 -17
  41. data/config/locales/ca.yml +4 -38
  42. data/config/locales/cs.yml +4 -38
  43. data/config/locales/de.yml +4 -38
  44. data/config/locales/el.yml +0 -31
  45. data/config/locales/en.yml +5 -39
  46. data/config/locales/es-MX.yml +4 -38
  47. data/config/locales/es-PY.yml +4 -38
  48. data/config/locales/es.yml +4 -38
  49. data/config/locales/eu.yml +0 -17
  50. data/config/locales/fi-plain.yml +5 -39
  51. data/config/locales/fi.yml +5 -39
  52. data/config/locales/fr-CA.yml +1 -39
  53. data/config/locales/fr-LU.yml +449 -0
  54. data/config/locales/fr.yml +0 -38
  55. data/config/locales/gl.yml +4 -38
  56. data/config/locales/hu.yml +0 -31
  57. data/config/locales/id-ID.yml +0 -17
  58. data/config/locales/is-IS.yml +0 -14
  59. data/config/locales/it.yml +6 -39
  60. data/config/locales/ja.yml +4 -33
  61. data/config/locales/lb-LU.yml +1 -0
  62. data/config/locales/lv.yml +0 -30
  63. data/config/locales/nl.yml +5 -39
  64. data/config/locales/no.yml +0 -38
  65. data/config/locales/pl.yml +4 -41
  66. data/config/locales/pt-BR.yml +82 -17
  67. data/config/locales/pt.yml +0 -31
  68. data/config/locales/ro-RO.yml +5 -36
  69. data/config/locales/ru.yml +0 -17
  70. data/config/locales/sk.yml +0 -16
  71. data/config/locales/sl.yml +0 -17
  72. data/config/locales/sr-CS.yml +0 -16
  73. data/config/locales/sv.yml +3 -36
  74. data/config/locales/tr-TR.yml +0 -31
  75. data/config/locales/uk.yml +0 -17
  76. data/config/locales/zh-CN.yml +0 -31
  77. data/db/migrate/20210507063604_add_announcement_to_assemblies.rb +7 -0
  78. data/lib/decidim/api/assembly_type.rb +10 -1
  79. data/lib/decidim/assemblies/admin_engine.rb +122 -30
  80. data/lib/decidim/assemblies/engine.rb +6 -9
  81. data/lib/decidim/assemblies/participatory_space.rb +46 -8
  82. data/lib/decidim/assemblies/test/factories.rb +1 -0
  83. data/lib/decidim/assemblies/version.rb +1 -1
  84. metadata +22 -23
  85. data/app/assets/config/admin/decidim_assemblies_manifest.js +0 -2
  86. data/app/assets/config/decidim_assemblies_manifest.js +0 -2
  87. data/app/assets/javascripts/decidim/assemblies/admin/assemblies.js.es6 +0 -67
  88. data/app/assets/javascripts/decidim/assemblies/assemblies.js.es6 +0 -18
  89. data/app/assets/javascripts/decidim/assemblies/orgchart.js.es6 +0 -698
  90. data/app/cells/decidim/assemblies/statistic/show.erb +0 -9
  91. data/app/cells/decidim/assemblies/statistic_cell.rb +0 -20
  92. data/app/cells/decidim/assemblies/statistics/show.erb +0 -17
  93. data/app/cells/decidim/assemblies/statistics_cell.rb +0 -18
  94. data/config/locales/ja-JP.yml +0 -471
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7780d892e6468fdd163051447390c4edbb84d9cb4f2e38e6a634e30fc987961f
4
- data.tar.gz: d48fc9be1cf5acae50f06da3aad41b52333964efa0dfe3e85a1301c7dad11fca
3
+ metadata.gz: dccd4227a991494f21e7140b9cc73637a5101ef72f067c02ec3f158f14f2f1b9
4
+ data.tar.gz: 22fb5a987f077d84b3f3ad579a5b1bd6ce299ed695e9ae3d4c22444700bb9050
5
5
  SHA512:
6
- metadata.gz: ea12b06eadcdbfb3ac279f061ec8b6d0c5226391bae29347c3b0810b7b658df808451ec794a2f3c5269786244f566fb2215bf27bf7db2967b4f9bc7d923c6cdc
7
- data.tar.gz: e368d64d4232e808fe80646ee00f7df8719d54379dacbe89da6c79d446a7567390b43f0ff7358580f7179085a2bab039124e0f339ee020e078900d9959ecb674
6
+ metadata.gz: 0db6b26a03e997a08cde11975ce77340664655d7501f056885f8cc0484fa1a4b5f31f1b2f18656bacc9f15cf094839afe03e25d605b0e77c2b805778a1b07e2b
7
+ data.tar.gz: 00bf4f2c1965414698ab705e3096e3fd0e696ba4a09702fe760e96462d2b020869daa2c0b64d5aea2462b6ec5b2d95235d420b1064f1268546be9386dc023542
@@ -27,7 +27,7 @@ module Decidim
27
27
  end
28
28
 
29
29
  def resource_image_path
30
- model.hero_image.url
30
+ model.attached_uploader(:hero_image).path
31
31
  end
32
32
 
33
33
  def statuses
@@ -24,7 +24,7 @@
24
24
  <% end %>
25
25
  </div>
26
26
  <% else %>
27
- <div class="author__avatar"><%= image_tag asset_path("decidim/default-avatar.svg"), alt: "member-avatar" %></div>
27
+ <div class="author__avatar"><%= image_tag asset_pack_path("media/images/default-avatar.svg"), alt: "member-avatar" %></div>
28
28
  <div>
29
29
  <div class="author__name--container">
30
30
  <div class="author__name">
@@ -8,7 +8,7 @@
8
8
  <div class="column">
9
9
  <%= link_to decidim_assemblies.assembly_path(assembly), class: "card card--assembly card--mini" do %>
10
10
  <div aria-hidden="true" class="card__image-top"
11
- style="background-image:url(<%= assembly.hero_image.url %>)"></div>
11
+ style="background-image:url(<%= assembly.attached_uploader(:hero_image).path %>)"></div>
12
12
  <div class="card__content">
13
13
  <span class="card__title card__link"><%= translated_attribute assembly.title %></span>
14
14
  </div>
@@ -26,6 +26,7 @@ module Decidim
26
26
 
27
27
  Assembly.transaction do
28
28
  copy_assembly
29
+ copy_assembly_attachments
29
30
  copy_assembly_categories if @form.copy_categories?
30
31
  copy_assembly_components if @form.copy_components?
31
32
  end
@@ -46,8 +47,6 @@ module Decidim
46
47
  hashtag: @assembly.hashtag,
47
48
  description: @assembly.description,
48
49
  short_description: @assembly.short_description,
49
- hero_image: @assembly.hero_image,
50
- banner_image: @assembly.banner_image,
51
50
  promoted: @assembly.promoted,
52
51
  scope: @assembly.scope,
53
52
  parent: @assembly.parent,
@@ -57,10 +56,19 @@ module Decidim
57
56
  target: @assembly.target,
58
57
  participatory_scope: @assembly.participatory_scope,
59
58
  participatory_structure: @assembly.participatory_structure,
60
- meta_scope: @assembly.meta_scope
59
+ meta_scope: @assembly.meta_scope,
60
+ announcement: @assembly.announcement
61
61
  )
62
62
  end
63
63
 
64
+ def copy_assembly_attachments
65
+ [:hero_image, :banner_image].each do |attribute|
66
+ next unless @assembly.attached_uploader(attribute).attached?
67
+
68
+ @copied_assembly.send(attribute).attach(@assembly.send(attribute).blob)
69
+ end
70
+ end
71
+
64
72
  def copy_assembly_categories
65
73
  @assembly.categories.each do |category|
66
74
  Category.create!(
@@ -82,7 +82,8 @@ module Decidim
82
82
  facebook_handler: form.facebook_handler,
83
83
  instagram_handler: form.instagram_handler,
84
84
  youtube_handler: form.youtube_handler,
85
- github_handler: form.github_handler
85
+ github_handler: form.github_handler,
86
+ announcement: form.announcement
86
87
  )
87
88
  end
88
89
 
@@ -67,12 +67,16 @@ module Decidim
67
67
  )
68
68
  end
69
69
 
70
+ def followers
71
+ form.user.is_a?(Decidim::UserGroup) ? form.user.users : [form.user]
72
+ end
73
+
70
74
  def notify_assembly_member_about_new_membership
71
75
  data = {
72
76
  event: "decidim.events.assemblies.create_assembly_member",
73
77
  event_class: Decidim::Assemblies::CreateAssemblyMemberEvent,
74
78
  resource: assembly,
75
- followers: [form.user]
79
+ followers: followers
76
80
  }
77
81
  Decidim::EventsManager.publish(data)
78
82
  end
@@ -6,6 +6,8 @@ module Decidim
6
6
  # A command with all the business logic when creating a new participatory
7
7
  # assembly in the system.
8
8
  class UpdateAssembly < Rectify::Command
9
+ include ::Decidim::AttachmentAttributesMethods
10
+
9
11
  # Public: Initializes the command.
10
12
  #
11
13
  # assembly - the Assembly to update
@@ -95,17 +97,11 @@ module Decidim
95
97
  instagram_handler: form.instagram_handler,
96
98
  youtube_handler: form.youtube_handler,
97
99
  github_handler: form.github_handler,
98
- weight: form.weight
99
- }.merge(uploader_attributes)
100
- end
101
-
102
- def uploader_attributes
103
- {
104
- hero_image: form.hero_image,
105
- remove_hero_image: form.remove_hero_image,
106
- banner_image: form.banner_image,
107
- remove_banner_image: form.remove_banner_image
108
- }.delete_if { |_k, val| val.is_a?(Decidim::ApplicationUploader) }
100
+ weight: form.weight,
101
+ announcement: form.announcement
102
+ }.merge(
103
+ attachment_attributes(:hero_image, :banner_image)
104
+ )
109
105
  end
110
106
 
111
107
  def participatory_processes(assembly)
@@ -87,11 +87,7 @@ module Decidim
87
87
  end
88
88
 
89
89
  def assembly_params
90
- {
91
- id: params[:slug],
92
- hero_image: current_assembly.hero_image,
93
- banner_image: current_assembly.banner_image
94
- }.merge(params[:assembly].to_unsafe_h)
90
+ { id: params[:slug] }.merge(params[:assembly].to_unsafe_h)
95
91
  end
96
92
  end
97
93
  end
@@ -30,6 +30,7 @@ module Decidim
30
30
  translatable_attribute :subtitle, String
31
31
  translatable_attribute :target, String
32
32
  translatable_attribute :title, String
33
+ translatable_attribute :announcement, String
33
34
 
34
35
  attribute :created_by, String
35
36
  attribute :facebook_handler, String
@@ -60,8 +61,8 @@ module Decidim
60
61
 
61
62
  attribute :banner_image
62
63
  attribute :hero_image
63
- attribute :remove_banner_image
64
- attribute :remove_hero_image
64
+ attribute :remove_banner_image, Boolean, default: false
65
+ attribute :remove_hero_image, Boolean, default: false
65
66
 
66
67
  validates :area, presence: true, if: proc { |object| object.area_id.present? }
67
68
 
@@ -8,6 +8,7 @@ module Decidim
8
8
  #
9
9
  class AssemblyImportForm < Form
10
10
  include TranslatableAttributes
11
+ include Decidim::HasUploadValidations
11
12
 
12
13
  JSON_MIME_TYPE = "application/json"
13
14
  # Accepted mime types
@@ -34,7 +34,7 @@ module Decidim
34
34
  end
35
35
 
36
36
  def user
37
- @user ||= current_organization.users.find_by(id: user_id)
37
+ @user ||= current_organization.user_entities.find_by(id: user_id)
38
38
  end
39
39
 
40
40
  def positions_for_select
@@ -4,11 +4,6 @@ module Decidim
4
4
  module Assemblies
5
5
  module Admin
6
6
  module AssembliesAdminMenuHelper
7
- include Decidim::Admin::SidebarMenuHelper
8
-
9
- def admin_assemblies_menu
10
- @admin_assemblies_menu ||= sidebar_menu(:admin_assemblies_menu)
11
- end
12
7
  end
13
8
  end
14
9
  end
@@ -40,7 +40,7 @@ module Decidim
40
40
 
41
41
  translatable_fields :title, :subtitle, :short_description, :description, :developer_group, :meta_scope, :local_area,
42
42
  :target, :participatory_scope, :participatory_structure, :purpose_of_action, :composition, :created_by_other,
43
- :closing_date_reason, :internal_organisation, :special_features
43
+ :closing_date_reason, :internal_organisation, :special_features, :announcement
44
44
 
45
45
  belongs_to :organization,
46
46
  foreign_key: "decidim_organization_id",
@@ -69,11 +69,11 @@ module Decidim
69
69
  has_many :children, foreign_key: "parent_id", class_name: "Decidim::Assembly", inverse_of: :parent, dependent: :destroy
70
70
  belongs_to :parent, class_name: "Decidim::Assembly", inverse_of: :children, optional: true, counter_cache: :children_count
71
71
 
72
- validates_upload :hero_image
73
- mount_uploader :hero_image, Decidim::HeroImageUploader
72
+ has_one_attached :hero_image
73
+ validates_upload :hero_image, uploader: Decidim::HeroImageUploader
74
74
 
75
- validates_upload :banner_image
76
- mount_uploader :banner_image, Decidim::BannerImageUploader
75
+ has_one_attached :banner_image
76
+ validates_upload :banner_image, uploader: Decidim::BannerImageUploader
77
77
 
78
78
  validates :slug, uniqueness: { scope: :organization }
79
79
  validates :slug, presence: true, format: { with: Decidim::Assembly.slug_format }
@@ -9,7 +9,7 @@ module Decidim
9
9
 
10
10
  POSITIONS = %w(president vice_president secretary other).freeze
11
11
 
12
- belongs_to :user, foreign_key: "decidim_user_id", class_name: "Decidim::User", optional: true
12
+ belongs_to :user, foreign_key: "decidim_user_id", class_name: "Decidim::UserBaseEntity", optional: true
13
13
  belongs_to :assembly, foreign_key: "decidim_assembly_id", class_name: "Decidim::Assembly"
14
14
  alias participatory_space assembly
15
15
 
@@ -0,0 +1,5 @@
1
+ import "src/decidim/assemblies/assemblies"
2
+ import "src/decidim/assemblies/orgchart"
3
+
4
+ // Images
5
+ require.context("../images", true)
@@ -0,0 +1,3 @@
1
+ import "src/decidim/assemblies/admin/assemblies"
2
+ import "src/decidim/assemblies/admin/assembly_members"
3
+ import "src/decidim/slug_form"
@@ -0,0 +1,63 @@
1
+ $(() => {
2
+ const $assemblyScopeEnabled = $("#assembly_scopes_enabled");
3
+ const $assemblyScopeId = $("#assembly_scope_id");
4
+
5
+ if ($(".edit_assembly, .new_assembly").length > 0) {
6
+ $assemblyScopeEnabled.on("change", (event) => {
7
+ const checked = event.target.checked;
8
+ window.theDataPicker.enabled($assemblyScopeId, checked);
9
+ })
10
+ window.theDataPicker.enabled($assemblyScopeId, $assemblyScopeEnabled.prop("checked"));
11
+ }
12
+
13
+ const $form = $(".assembly_form_admin");
14
+
15
+ if ($form.length > 0) {
16
+
17
+ const $privateSpace = $form.find("#private_space");
18
+ const $isTransparent = $form.find("#is_transparent");
19
+ const $specialFeatures = $form.find("#special_features");
20
+
21
+
22
+ const toggleDisabledHiddenFields = () => {
23
+ const enabledPrivateSpace = $privateSpace.find("input[type='checkbox']").prop("checked");
24
+ $isTransparent.find("input[type='checkbox']").attr("disabled", "disabled");
25
+ $specialFeatures.hide();
26
+
27
+ if (enabledPrivateSpace) {
28
+ $isTransparent.find("input[type='checkbox']").attr("disabled", !enabledPrivateSpace);
29
+ $specialFeatures.show();
30
+ }
31
+ };
32
+
33
+ $privateSpace.on("change", toggleDisabledHiddenFields);
34
+ toggleDisabledHiddenFields();
35
+
36
+ const $assemblyType = $form.find("#assembly_assembly_type");
37
+ const $assemblyTypeOther = $form.find("#assembly_type_other");
38
+
39
+ const $assemblyCreatedBy = $form.find("#assembly_created_by");
40
+ const $assemblyCreatedByOther = $form.find("#created_by_other");
41
+
42
+ const toggleDependsOnSelect = ($target, $showDiv) => {
43
+ const value = $target.val();
44
+ $showDiv.hide();
45
+ if (value === "others") {
46
+ $showDiv.show();
47
+ }
48
+ };
49
+
50
+ $assemblyType.on("change", (ev) => {
51
+ const $target = $(ev.target);
52
+ toggleDependsOnSelect($target, $assemblyTypeOther);
53
+ });
54
+
55
+ $assemblyCreatedBy.on("change", (ev) => {
56
+ const $target = $(ev.target);
57
+ toggleDependsOnSelect($target, $assemblyCreatedByOther);
58
+ });
59
+
60
+ toggleDependsOnSelect($assemblyType, $assemblyTypeOther);
61
+ toggleDependsOnSelect($assemblyCreatedBy, $assemblyCreatedByOther);
62
+ }
63
+ });
@@ -1,6 +1,6 @@
1
- ((exports) => {
2
- const { createFieldDependentInputs } = exports.DecidimAdmin;
1
+ import createFieldDependentInputs from "src/decidim/admin/field_dependent_inputs.component"
3
2
 
3
+ $(() => {
4
4
  const $assemblyMemberType = $("#assembly_member_existing_user");
5
5
 
6
6
  createFieldDependentInputs({
@@ -34,4 +34,4 @@
34
34
  return $field.val() === "other"
35
35
  }
36
36
  });
37
- })(window);
37
+ })
@@ -0,0 +1,14 @@
1
+ $(() => {
2
+ $(".show-more").on("click", function() {
3
+ /* eslint-disable no-invalid-this */
4
+ $(this).hide();
5
+ $(".show-more-panel").removeClass("hide");
6
+ $(".hide-more").show();
7
+ });
8
+
9
+ $(".hide-more").on("click", function() {
10
+ $(this).hide();
11
+ $(".show-more-panel").addClass("hide");
12
+ $(".show-more").show();
13
+ });
14
+ })
@@ -0,0 +1,695 @@
1
+ /* eslint-disable require-jsdoc, max-lines, no-return-assign, func-style, id-length, no-plusplus, no-use-before-define, no-negated-condition, init-declarations, no-invalid-this, no-param-reassign, no-ternary, multiline-ternary, no-nested-ternary, no-eval, no-extend-native, prefer-reflect */
2
+ /* eslint dot-location: ["error", "property"], no-negated-condition: "error" */
3
+ /* eslint no-unused-expressions: ["error", { "allowTernary": true }] */
4
+ /* eslint no-unused-vars: 0 */
5
+ /* global d3 */
6
+
7
+ import * as d3 from "d3"
8
+ import renderChart from "src/decidim/vizzs/renders"
9
+
10
+ // lib
11
+ const renderOrgCharts = () => {
12
+ const $orgChartContainer = $(".js-orgchart")
13
+ const $btnReset = $(".js-reset-orgchart")
14
+
15
+ let dataDepicted = null
16
+ let fake = false
17
+ let orgchart = {}
18
+
19
+ // lib - https://bl.ocks.org/bumbeishvili/b96ba47ea21d14dfce6ebb859b002d3a
20
+ const renderChartCollapsibleNetwork = (params) => {
21
+
22
+ // exposed variables
23
+ let attrs = {
24
+ id: `id${Math.floor(Math.random() * 1000000)}`,
25
+ svgWidth: 960,
26
+ svgHeight: 600,
27
+ marginTop: 0,
28
+ marginBottom: 5,
29
+ marginRight: 0,
30
+ marginLeft: 30,
31
+ container: "body",
32
+ distance: 150,
33
+ hiddenChildLevel: 1,
34
+ hoverOpacity: 0.2,
35
+ maxTextDisplayZoomLevel: 1,
36
+ lineStrokeWidth: 1.5,
37
+ fakeRoot: false,
38
+ nodeGutter: { x: 16, y: 8 },
39
+ childrenIndicatorRadius: 15,
40
+ fakeBorderWidth: 32,
41
+ data: null
42
+ }
43
+
44
+ /* ############### IF EXISTS OVERWRITE ATTRIBUTES FROM PASSED PARAM ####### */
45
+
46
+ let attrKeys = Object.keys(attrs)
47
+ attrKeys.forEach(function (key) {
48
+ if (params && params[key]) {
49
+ attrs[key] = params[key]
50
+ }
51
+ })
52
+
53
+ // innerFunctions which will update visuals
54
+ let updateData
55
+ let collapse, expand
56
+ let filter
57
+ let hierarchy = {}
58
+
59
+ // main chart object
60
+ let main = function (selection) {
61
+ selection.each(function scope() {
62
+
63
+ // calculated properties
64
+ let calc = {}
65
+ calc.chartLeftMargin = attrs.marginLeft
66
+ calc.chartTopMargin = attrs.marginTop
67
+ calc.chartWidth = attrs.svgWidth - attrs.marginRight - calc.chartLeftMargin
68
+ calc.chartHeight = attrs.svgHeight - attrs.marginBottom - calc.chartTopMargin
69
+
70
+ // ########################## HIERARCHY STUFF #########################
71
+ hierarchy.root = d3.hierarchy(attrs.data.root)
72
+
73
+ // ########################### BEHAVIORS #########################
74
+ let behaviors = {}
75
+ // behaviors.zoom = d3.zoom().scaleExtent([0.75, 100, 8]).on("zoom", zoomed)
76
+ behaviors.drag = d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended)
77
+
78
+ // ########################### LAYOUTS #########################
79
+ let layouts = {}
80
+
81
+ // custom radial layout
82
+ layouts.radial = radial()
83
+
84
+ // ########################### FORCE STUFF #########################
85
+ let force = {}
86
+ force.link = d3.forceLink().id((d) => d.id)
87
+ force.charge = d3.forceManyBody().strength(-240)
88
+ force.center = d3.forceCenter(calc.chartWidth / 2, calc.chartHeight / 2)
89
+
90
+ // prevent collide
91
+ force.collide = d3.forceCollide().radius((d) => {
92
+ // Creates an invented radius based on element measures: diagonal = 2 * radius = sqrt(width^2, height^2)
93
+ let base = (d.bbox || {}).width + (attrs.nodeGutter.x * 2)
94
+ let height = (d.bbox || {}).height + (attrs.nodeGutter.y * 2)
95
+ let diagonal = Math.sqrt(Math.pow(base, 2) + Math.pow(height, 2))
96
+ let fakeRadius = (diagonal / 2)
97
+
98
+ // return d3.max([attrs.nodeDistance * 3, fakeRadius])
99
+ return fakeRadius * 1.5
100
+ })
101
+
102
+ // manually set x positions (which is calculated using custom radial layout)
103
+ force.x = d3.forceX()
104
+ .strength(0.5)
105
+ .x(function (d) {
106
+
107
+ // if node does not have children and is channel (depth=2) , then position it on parent's coordinate
108
+ if (!d.children && d.depth > 2) {
109
+ if (d.parent) {
110
+ d = d.parent
111
+ }
112
+ }
113
+
114
+ // custom circle projection - radius will be - (d.depth - 1) * 150
115
+ return projectCircle(d.proportion, (d.depth - 1) * attrs.distance)[0]
116
+ })
117
+
118
+ // manually set y positions (which is calculated using d3.cluster)
119
+ force.y = d3.forceY()
120
+ .strength(0.5)
121
+ .y(function (d) {
122
+
123
+ // if node does not have children and is channel (depth=2) , then position it on parent's coordinate
124
+ if (!d.children && d.depth > 2) {
125
+ if (d.parent) {
126
+ d = d.parent
127
+ }
128
+ }
129
+
130
+ // custom circle projection - radius will be - (d.depth - 1) * 150
131
+ return projectCircle(d.proportion, (d.depth - 1) * attrs.distance)[1]
132
+ })
133
+
134
+ // --------------------------------- INITIALISE FORCE SIMULATION ----------------------------
135
+
136
+ // get based on top parameter simulation
137
+ force.simulation = d3.forceSimulation()
138
+ .force("link", force.link)
139
+ .force("charge", force.charge)
140
+ .force("center", force.center)
141
+ .force("collide", force.collide)
142
+ .force("x", force.x)
143
+ .force("y", force.y)
144
+
145
+ // ########################### HIERARCHY STUFF #########################
146
+
147
+ // flatten root
148
+ let arr = flatten(hierarchy.root)
149
+
150
+ // hide members based on their depth
151
+ arr.forEach((d) => {
152
+ // Hide fake root node
153
+ if ((attrs.fakeRoot) && (d.depth === 1)) {
154
+ d.hidden = true
155
+ }
156
+
157
+ if (d.depth > attrs.hiddenChildLevel) {
158
+ d._children = d.children
159
+ d.children = null
160
+ }
161
+ })
162
+
163
+ // #################################### DRAWINGS #######################
164
+
165
+ // drawing containers
166
+ let container = d3.select(this)
167
+
168
+ // add svg
169
+ let svg = container.patternify({ tag: "svg", selector: "svg-chart-container" })
170
+ .attr("width", attrs.svgWidth)
171
+ .attr("height", attrs.svgHeight)
172
+ // .call(behaviors.zoom)
173
+
174
+ // add container g element
175
+ let chart = svg.patternify({ tag: "g", selector: "chart" })
176
+ .attr("transform", `translate(${calc.chartLeftMargin},${calc.chartTopMargin})`)
177
+
178
+ // ################################ Chart Content Drawing ##################################
179
+
180
+ // link wrapper
181
+ let linksWrapper = chart.patternify({ tag: "g", selector: "links-wrapper" })
182
+
183
+ // node wrapper
184
+ let nodesWrapper = chart.patternify({ tag: "g", selector: "nodes-wrapper" })
185
+ let links, nodes
186
+
187
+ // reusable function which updates visual based on data change
188
+ update()
189
+
190
+ // update visual based on data change
191
+ function update(clickedNode) {
192
+
193
+ // Show/hide reset button
194
+ (clickedNode) ? $btnReset.removeClass("invisible") : $btnReset.addClass("invisible")
195
+
196
+ // set xy and proportion properties with custom radial layout
197
+ layouts.radial(hierarchy.root)
198
+
199
+ // nodes and links array
200
+ let nodesArr = flatten(hierarchy.root, true)
201
+ .orderBy((d) => d.depth)
202
+ .filter((d) => !d.hidden)
203
+
204
+ let linksArr = hierarchy.root.links()
205
+ .filter((d) => !d.source.hidden)
206
+ .filter((d) => !d.target.hidden)
207
+
208
+ // make new nodes to appear near the parents
209
+ nodesArr.forEach(function (d) {
210
+ if (clickedNode && clickedNode.id === (d.parent && d.parent.id)) {
211
+ d.x = d.parent.x
212
+ d.y = d.parent.y
213
+ }
214
+ })
215
+
216
+ // links
217
+ links = linksWrapper.selectAll(".link")
218
+ .data(linksArr, (d) => d.target.id)
219
+ links.exit().remove()
220
+
221
+ links = links.enter()
222
+ .append("line")
223
+ .attr("class", "link")
224
+ .merge(links)
225
+
226
+ // node groups
227
+ nodes = nodesWrapper.selectAll(".node")
228
+ .data(nodesArr, (d) => d.id)
229
+ nodes.exit().remove()
230
+
231
+ let enteredNodes = nodes.enter()
232
+ .append("g")
233
+ .attr("class", "node")
234
+
235
+ // bind event handlers
236
+ enteredNodes
237
+ .on("click", nodeClick)
238
+ .on("mouseenter", nodeMouseEnter)
239
+ .on("mouseleave", nodeMouseLeave)
240
+ .call(behaviors.drag)
241
+
242
+ // channels grandchildren
243
+ enteredNodes.append("rect")
244
+ .attr("class", "as-card")
245
+ .attr("rx", 4)
246
+ .attr("ry", 4)
247
+
248
+ enteredNodes.append("text")
249
+ .attr("class", "as-text")
250
+ .text((d) => d.data.name)
251
+
252
+ enteredNodes.selectAll("text").each(function(d) {
253
+ d.bbox = this.getBBox()
254
+ })
255
+
256
+ enteredNodes.selectAll("rect")
257
+ .attr("x", (d) => d.bbox.x - attrs.nodeGutter.x)
258
+ .attr("y", (d) => d.bbox.y - attrs.nodeGutter.y)
259
+ .attr("width", (d) => d.bbox.width + (2 * attrs.nodeGutter.x))
260
+ .attr("height", (d) => d.bbox.height + (2 * attrs.nodeGutter.y))
261
+
262
+ // append circle & text only when there are children
263
+ enteredNodes
264
+ .append("circle")
265
+ .filter((d) => Boolean(d.children) || Boolean(d._children))
266
+ .attr("class", "as-circle")
267
+ .attr("r", attrs.childrenIndicatorRadius)
268
+ .attr("cx", (d) => d.bbox.x + d.bbox.width + attrs.nodeGutter.x)
269
+ .attr("cy", (d) => d.bbox.y + d.bbox.height + attrs.nodeGutter.y)
270
+
271
+ enteredNodes
272
+ .append("text")
273
+ .filter((d) => Boolean(d.children) || Boolean(d._children))
274
+ .attr("class", "as-text")
275
+ .attr("dx", (d) => d.bbox.x + d.bbox.width + attrs.nodeGutter.x)
276
+ .attr("dy", attrs.childrenIndicatorRadius + 3)
277
+ .text((d) => d3.max([(d.children || {}).length, (d._children || {}).length]))
278
+
279
+ // merge node groups and style it
280
+ nodes = enteredNodes.merge(nodes)
281
+
282
+ // force simulation
283
+ force.simulation.nodes(nodesArr).on("tick", ticked)
284
+
285
+ // links simulation
286
+ force.simulation.force("link").links(links).id((d) => d.id).distance(attrs.distance * 2).strength(2)
287
+ }
288
+
289
+ // ####################################### EVENT HANDLERS ########################
290
+
291
+ // zoom handler
292
+ // function zoomed() {
293
+ // // get transform event
294
+ // let transform = d3.event.transform
295
+ // attrs.lastTransform = transform
296
+ //
297
+ // // apply transform event props to the wrapper
298
+ // chart.attr("transform", transform)
299
+ //
300
+ // svg.selectAll(".node").attr("transform", (d) => `translate(${d.x},${d.y}) scale(${1 / (attrs.lastTransform ? attrs.lastTransform.k : 1)})`)
301
+ // svg.selectAll(".link").attr("stroke-width", attrs.lineStrokeWidth / (attrs.lastTransform ? attrs.lastTransform.k : 1))
302
+ // }
303
+
304
+ // tick handler
305
+ function ticked() {
306
+ const fakeBorderWidth = attrs.fakeBorderWidth
307
+ const maxXValueAvailable = (value) => Math.max(Math.min(calc.chartWidth - fakeBorderWidth, value), fakeBorderWidth)
308
+ const maxYValueAvailable = (value) => Math.max(Math.min(calc.chartHeight - fakeBorderWidth, value), fakeBorderWidth)
309
+ // set links position
310
+ links
311
+ .attr("x1", (d) => maxXValueAvailable(d.source.x))
312
+ .attr("y1", (d) => maxYValueAvailable(d.source.y))
313
+ .attr("x2", (d) => maxXValueAvailable(d.target.x))
314
+ .attr("y2", (d) => maxYValueAvailable(d.target.y))
315
+
316
+ // set nodes position
317
+ svg.selectAll(".node")
318
+ .attr("transform", (d) => `translate(${maxXValueAvailable(d.x)},${maxYValueAvailable(d.y)})`)
319
+ }
320
+
321
+ // handler drag start event
322
+ function dragstarted() {
323
+ // disable node fixing
324
+ nodes.each((d) => {
325
+ d.fx = null
326
+ d.fy = null
327
+ })
328
+ }
329
+
330
+ // handle dragging event
331
+ function dragged(d) {
332
+ // make dragged node fixed
333
+ d.fx = d3.event.x
334
+ d.fy = d3.event.y
335
+ }
336
+
337
+ // -------------------- handle drag end event ---------------
338
+ function dragended() {
339
+ // we are doing nothing, here , aren't we?
340
+ }
341
+
342
+ // -------------------------- node mouse hover handler ---------------
343
+ function nodeMouseEnter(d) {
344
+ // get links
345
+ let _links = hierarchy.root.links()
346
+
347
+ // get hovered node connected links
348
+ let connectedLinks = _links.filter((l) => l.source.id === d.id || l.target.id === d.id)
349
+
350
+ // get hovered node linked nodes
351
+ let linkedNodes = connectedLinks.map((s) => s.source.id).concat(connectedLinks.map((c) => c.target.id))
352
+
353
+ // reduce all other nodes opacity
354
+ nodesWrapper.selectAll(".node")
355
+ .filter((n) => linkedNodes.indexOf(n.id) === -1)
356
+ .attr("opacity", attrs.hoverOpacity)
357
+
358
+ // reduce all other links opacity
359
+ linksWrapper.selectAll(".link")
360
+ .attr("opacity", attrs.hoverOpacity)
361
+
362
+ // highlight hovered nodes connections
363
+ linksWrapper.selectAll(".link")
364
+ .filter((l) => l.source.id === d.id || l.target.id === d.id)
365
+ .attr("opacity", 1)
366
+ }
367
+
368
+ // --------------- handle mouseleave event ---------------
369
+ function nodeMouseLeave() {
370
+ // return things back to normal
371
+ nodesWrapper.selectAll(".node")
372
+ .attr("opacity", 1)
373
+ linksWrapper.selectAll(".link")
374
+ .attr("opacity", 1)
375
+ }
376
+
377
+ // --------------- handle node click event ---------------
378
+ function nodeClick(d) {
379
+ // free fixed nodes
380
+ nodes.each((di) => {
381
+ di.fx = null
382
+ di.fy = null
383
+ })
384
+
385
+ // collapse or expand node
386
+ if (d.children) {
387
+ collapse(d)
388
+ } else if (d._children) {
389
+ expand(d)
390
+ } else {
391
+ // nothing is to collapse or expand
392
+ }
393
+
394
+ freeNodes()
395
+ }
396
+
397
+ // ######################################### UTIL FUNCS ##################################
398
+ updateData = function () {
399
+ main.run()
400
+ }
401
+
402
+ collapse = function (d, deep = false) {
403
+ if (d.children) {
404
+ if (deep) {
405
+ d.children.forEach((e) => collapse(e, true))
406
+ }
407
+
408
+ d._children = d.children
409
+ d.children = null
410
+ }
411
+
412
+ update(d)
413
+ force.simulation.restart()
414
+ force.simulation.alphaTarget(0.15)
415
+ }
416
+
417
+ expand = function (d, deep = false) {
418
+ if (d._children) {
419
+ if (deep) {
420
+ d._children.forEach((e) => expand(e, true))
421
+ }
422
+
423
+ d.children = d._children
424
+ d._children = null
425
+ }
426
+
427
+ update(d)
428
+ force.simulation.restart()
429
+ force.simulation.alphaTarget(0.15)
430
+ }
431
+
432
+ // function slowDownNodes() {
433
+ // force.simulation.alphaTarget(0.05)
434
+ // }
435
+
436
+ // function speedUpNodes() {
437
+ // force.simulation.alphaTarget(0.45)
438
+ // }
439
+
440
+ function freeNodes() {
441
+ d3.selectAll(".node").each((n) => {
442
+ n.fx = null
443
+ n.fy = null
444
+ })
445
+ }
446
+
447
+ function projectCircle(value, radius) {
448
+ let r = radius || 0
449
+ let corner = value * 2 * Math.PI
450
+ return [Math.sin(corner) * r, -Math.cos(corner) * r]
451
+ }
452
+
453
+ // recursively loop on children and extract nodes as an array
454
+ function flatten(root, clustered) {
455
+ let nodesArray = []
456
+ let i = 0
457
+ function recurse(node, depth) {
458
+ if (node.children) {
459
+ node.children.forEach(function (child) {
460
+ recurse(child, depth + 1)
461
+ })
462
+ }
463
+
464
+ if (!node.id) {
465
+ node.id = ++i
466
+ } else {
467
+ ++i
468
+ }
469
+
470
+ node.depth = depth
471
+ if (clustered) {
472
+ if (!node.cluster) {
473
+ // if cluster coordinates are not set, set it
474
+ node.cluster = { x: node.x, y: node.y }
475
+ }
476
+ }
477
+ nodesArray.push(node)
478
+ }
479
+ recurse(root, 1)
480
+ return nodesArray
481
+ }
482
+
483
+ function debug() {
484
+ if (attrs.isDebug) {
485
+ // stringify func
486
+ let stringified = String(scope)
487
+
488
+ // parse variable names
489
+ let groupVariables = stringified
490
+ // match var x-xx= {}
491
+ .match(/var\s+([\w])+\s*=\s*{\s*}/gi)
492
+ // match xxx
493
+ .map((d) => d.match(/\s+\w*/gi).filter((s) => s.trim()))
494
+ // get xxx
495
+ .map((v) => v[0].trim())
496
+
497
+ // assign local variables to the scope
498
+ groupVariables.forEach((v) => {
499
+ main[`P_${v}`] = eval(v)
500
+ })
501
+ }
502
+ }
503
+
504
+ debug()
505
+
506
+ })
507
+ }
508
+
509
+ // ----------- PROTOTYEPE FUNCTIONS ----------------------
510
+ d3.selection.prototype.patternify = function (_params) {
511
+ let selector = _params.selector
512
+ let elementTag = _params.tag
513
+ let _data = _params.data || [selector]
514
+
515
+ // pattern in action
516
+ let selection = this.selectAll(`.${selector}`).data(_data)
517
+ selection.exit().remove()
518
+ selection = selection.enter().append(elementTag).merge(selection)
519
+ selection.attr("class", selector)
520
+
521
+ return selection
522
+ }
523
+
524
+ // custom radial layout
525
+ function radial() {
526
+ return function (root) {
527
+
528
+ recurse(root, 0, 1)
529
+
530
+ function recurse(node, min, max) {
531
+ node.proportion = (max + min) / 2
532
+ if (!node.x) {
533
+
534
+ // if node has parent, match entered node positions to it's parent
535
+ if (node.parent) {
536
+ node.x = node.parent.x
537
+ } else {
538
+ node.x = 0
539
+ }
540
+ }
541
+
542
+ // if node had parent, match entered node positions to it's parent
543
+ if (!node.y) {
544
+ if (node.parent) {
545
+ node.y = node.parent.y
546
+ } else {
547
+ node.y = 0
548
+ }
549
+ }
550
+
551
+ // recursively do the same for children
552
+ if (node.children) {
553
+ let offset = (max - min) / node.children.length
554
+ node.children.forEach(function (child, i) {
555
+ let newMin = min + (offset * i)
556
+ let newMax = newMin + offset
557
+
558
+ recurse(child, newMin, newMax)
559
+ })
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ // https://github.com/bumbeishvili/d3js-boilerplates#orderby
566
+ Array.prototype.orderBy = function (func) {
567
+ this.sort((_a, _b) => {
568
+ let a = func(_a)
569
+ let b = func(_b)
570
+ if (typeof a === "string" || a instanceof String) {
571
+ return a.localeCompare(b)
572
+ }
573
+ return a - b
574
+ })
575
+
576
+ return this
577
+ }
578
+
579
+ // ########################## BOILEPLATE STUFF ################
580
+
581
+ // dinamic keys functions
582
+ Object.keys(attrs).forEach((key) => {
583
+ // Attach variables to main function
584
+ return main[key] = function (_) {
585
+ let string = `attrs['${key}'] = _`
586
+
587
+ if (!arguments.length) {
588
+ return eval(` attrs['${key}'];`)
589
+ }
590
+
591
+ eval(string)
592
+
593
+ return main
594
+ }
595
+ })
596
+
597
+ // set attrs as property
598
+ main.attrs = attrs
599
+
600
+ // debugging visuals
601
+ main.debug = function (isDebug) {
602
+ attrs.isDebug = isDebug
603
+ if (isDebug) {
604
+ if (!window.charts) {
605
+ window.charts = []
606
+ }
607
+ window.charts.push(main)
608
+ }
609
+ return main
610
+ }
611
+
612
+ // exposed update functions
613
+ main.data = function (value) {
614
+ if (!arguments.length) {
615
+ return attrs.data
616
+ }
617
+
618
+ attrs.data = value
619
+ if (typeof updateData === "function") {
620
+ updateData()
621
+ }
622
+ return main
623
+ }
624
+
625
+ // run visual
626
+ main.run = function () {
627
+ d3.selectAll(attrs.container)
628
+ .call(main)
629
+ return main
630
+ }
631
+
632
+ main.filter = function (filterParams) {
633
+ if (!arguments.length) {
634
+ return attrs.filterParams
635
+ }
636
+
637
+ attrs.filterParams = filterParams
638
+ if (typeof filter === "function") {
639
+ filter()
640
+ }
641
+ return main
642
+ }
643
+
644
+ main.reset = function () {
645
+
646
+ hierarchy.root.children.forEach((e) => collapse(e, true))
647
+ main.run()
648
+
649
+ return main
650
+ }
651
+
652
+ return main
653
+ }
654
+
655
+ // initialization
656
+ $orgChartContainer.each((i, container) => {
657
+
658
+ let $container = $(container)
659
+ let width = $container.width()
660
+ let height = width / (16 / 9)
661
+
662
+ d3.json($container.data("url")).then((data) => {
663
+ // Make a fake previous node if the data entry is not hierarchical
664
+ if (data instanceof Array) {
665
+ fake = true
666
+ dataDepicted = {
667
+ name: null,
668
+ children: data
669
+ }
670
+ } else {
671
+ dataDepicted = data
672
+ }
673
+
674
+ orgchart = renderChartCollapsibleNetwork()
675
+ .svgHeight(height)
676
+ .svgWidth(width)
677
+ .fakeRoot(fake)
678
+ .container(`#${container.id}`)
679
+ .data({
680
+ root: dataDepicted
681
+ })
682
+ .debug(true)
683
+ .run()
684
+ })
685
+ })
686
+
687
+ // reset
688
+ $btnReset.click(function() {
689
+ orgchart.reset()
690
+ })
691
+ }
692
+
693
+ $(() => {
694
+ renderChart(renderOrgCharts);
695
+ })