decidim-assemblies 0.24.3 → 0.25.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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
+ })