iron-cms 0.1.0

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 (240) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +174 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/iron.css +2 -0
  6. data/app/assets/config/iron_manifest.js +1 -0
  7. data/app/assets/images/iron/icons/sprite.svg +9425 -0
  8. data/app/assets/tailwind/iron/actiontext.css +550 -0
  9. data/app/assets/tailwind/iron/application.css +66 -0
  10. data/app/assets/tailwind/iron/components/button.css +74 -0
  11. data/app/assets/tailwind/iron/components/dropdown.css +42 -0
  12. data/app/assets/tailwind/iron/components/fieldset.css +18 -0
  13. data/app/assets/tailwind/iron/components/form.css +14 -0
  14. data/app/assets/tailwind/iron/components/icon-picker.css +9 -0
  15. data/app/assets/tailwind/iron/components/input.css +44 -0
  16. data/app/assets/tailwind/iron/components/page.css +7 -0
  17. data/app/assets/tailwind/iron/components/select.css +36 -0
  18. data/app/assets/tailwind/iron/components/sidebar.css +21 -0
  19. data/app/assets/tailwind/iron/components/textarea.css +27 -0
  20. data/app/controllers/concerns/iron/authentication.rb +66 -0
  21. data/app/controllers/concerns/iron/authorization.rb +11 -0
  22. data/app/controllers/concerns/iron/locale_aware.rb +20 -0
  23. data/app/controllers/concerns/iron/web_page.rb +18 -0
  24. data/app/controllers/iron/application_controller.rb +7 -0
  25. data/app/controllers/iron/block_definitions/field_definitions_controller.rb +8 -0
  26. data/app/controllers/iron/block_definitions_controller.rb +52 -0
  27. data/app/controllers/iron/blocks_controller.rb +12 -0
  28. data/app/controllers/iron/content_types/field_definitions_controller.rb +8 -0
  29. data/app/controllers/iron/content_types_controller.rb +52 -0
  30. data/app/controllers/iron/entries_controller.rb +103 -0
  31. data/app/controllers/iron/field_definitions_controller.rb +55 -0
  32. data/app/controllers/iron/first_runs_controller.rb +29 -0
  33. data/app/controllers/iron/home_controller.rb +6 -0
  34. data/app/controllers/iron/icons_controller.rb +10 -0
  35. data/app/controllers/iron/locales_controller.rb +52 -0
  36. data/app/controllers/iron/passwords_controller.rb +36 -0
  37. data/app/controllers/iron/references_controller.rb +11 -0
  38. data/app/controllers/iron/schemas_controller.rb +32 -0
  39. data/app/controllers/iron/sessions_controller.rb +32 -0
  40. data/app/controllers/iron/settings_controller.rb +23 -0
  41. data/app/controllers/iron/users_controller.rb +66 -0
  42. data/app/helpers/iron/application_helper.rb +25 -0
  43. data/app/helpers/iron/avatar_helper.rb +35 -0
  44. data/app/helpers/iron/block_helper.rb +4 -0
  45. data/app/helpers/iron/buttons_helper.rb +14 -0
  46. data/app/helpers/iron/components/badge_helper.rb +70 -0
  47. data/app/helpers/iron/components/dropdown_helper.rb +161 -0
  48. data/app/helpers/iron/content_types_helper.rb +11 -0
  49. data/app/helpers/iron/entries_helper.rb +13 -0
  50. data/app/helpers/iron/field_definitions_helper.rb +5 -0
  51. data/app/helpers/iron/form_builder.rb +43 -0
  52. data/app/helpers/iron/icons_helper.rb +30 -0
  53. data/app/helpers/iron/image_helper.rb +50 -0
  54. data/app/javascript/iron/application.js +6 -0
  55. data/app/javascript/iron/controllers/application.js +9 -0
  56. data/app/javascript/iron/controllers/element_controller.js +11 -0
  57. data/app/javascript/iron/controllers/file_upload_controller.js +18 -0
  58. data/app/javascript/iron/controllers/form_controller.js +7 -0
  59. data/app/javascript/iron/controllers/handle_controller.js +27 -0
  60. data/app/javascript/iron/controllers/icon_picker_controller.js +48 -0
  61. data/app/javascript/iron/controllers/index.js +4 -0
  62. data/app/javascript/iron/controllers/sortable_list_controller.js +125 -0
  63. data/app/javascript/iron/controllers/toggle_controller.js +14 -0
  64. data/app/javascript/iron/controllers/trix_controller.js +19 -0
  65. data/app/javascript/iron/lib/lexorank.js +51 -0
  66. data/app/jobs/iron/application_job.rb +4 -0
  67. data/app/jobs/iron/generate_entry_routes_job.rb +15 -0
  68. data/app/mailers/iron/application_mailer.rb +6 -0
  69. data/app/mailers/passwords_mailer.rb +6 -0
  70. data/app/models/concerns/iron/csv_serializable.rb +28 -0
  71. data/app/models/iron/account/joinable.rb +16 -0
  72. data/app/models/iron/account.rb +13 -0
  73. data/app/models/iron/application_record.rb +5 -0
  74. data/app/models/iron/block_definition/portable.rb +20 -0
  75. data/app/models/iron/block_definition.rb +10 -0
  76. data/app/models/iron/content_type/field_queryable.rb +101 -0
  77. data/app/models/iron/content_type/portable.rb +30 -0
  78. data/app/models/iron/content_type/titlable.rb +13 -0
  79. data/app/models/iron/content_type/web_publishable.rb +47 -0
  80. data/app/models/iron/content_type.rb +39 -0
  81. data/app/models/iron/content_types/collection.rb +6 -0
  82. data/app/models/iron/content_types/single.rb +9 -0
  83. data/app/models/iron/current.rb +12 -0
  84. data/app/models/iron/entry/deep_validation.rb +32 -0
  85. data/app/models/iron/entry/presentable.rb +40 -0
  86. data/app/models/iron/entry/schemable.rb +16 -0
  87. data/app/models/iron/entry/titlable.rb +35 -0
  88. data/app/models/iron/entry/web_publishable.rb +85 -0
  89. data/app/models/iron/entry.rb +16 -0
  90. data/app/models/iron/field/belongs_to_entry.rb +29 -0
  91. data/app/models/iron/field.rb +20 -0
  92. data/app/models/iron/field_definition/portable.rb +36 -0
  93. data/app/models/iron/field_definition.rb +42 -0
  94. data/app/models/iron/field_definitions/block.rb +23 -0
  95. data/app/models/iron/field_definitions/block_list.rb +8 -0
  96. data/app/models/iron/field_definitions/boolean.rb +7 -0
  97. data/app/models/iron/field_definitions/date.rb +7 -0
  98. data/app/models/iron/field_definitions/file.rb +67 -0
  99. data/app/models/iron/field_definitions/number.rb +7 -0
  100. data/app/models/iron/field_definitions/reference.rb +8 -0
  101. data/app/models/iron/field_definitions/reference_list.rb +8 -0
  102. data/app/models/iron/field_definitions/rich_text_area.rb +4 -0
  103. data/app/models/iron/field_definitions/text_area.rb +7 -0
  104. data/app/models/iron/field_definitions/text_field.rb +25 -0
  105. data/app/models/iron/fields/block.rb +40 -0
  106. data/app/models/iron/fields/block_list.rb +16 -0
  107. data/app/models/iron/fields/boolean.rb +7 -0
  108. data/app/models/iron/fields/date.rb +7 -0
  109. data/app/models/iron/fields/file.rb +29 -0
  110. data/app/models/iron/fields/number.rb +7 -0
  111. data/app/models/iron/fields/reference.rb +14 -0
  112. data/app/models/iron/fields/reference_list.rb +17 -0
  113. data/app/models/iron/fields/rich_text_area.rb +9 -0
  114. data/app/models/iron/fields/text_area.rb +7 -0
  115. data/app/models/iron/fields/text_field.rb +19 -0
  116. data/app/models/iron/first_run.rb +13 -0
  117. data/app/models/iron/icon_catalog.rb +45 -0
  118. data/app/models/iron/locale.rb +30 -0
  119. data/app/models/iron/reference.rb +8 -0
  120. data/app/models/iron/schema_archive.rb +71 -0
  121. data/app/models/iron/schema_exporter.rb +15 -0
  122. data/app/models/iron/schema_importer.rb +278 -0
  123. data/app/models/iron/session.rb +5 -0
  124. data/app/models/iron/user/role.rb +21 -0
  125. data/app/models/iron/user.rb +37 -0
  126. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  127. data/app/views/iron/block_definitions/_block_definition.html.erb +21 -0
  128. data/app/views/iron/block_definitions/_form.html.erb +46 -0
  129. data/app/views/iron/block_definitions/edit.html.erb +8 -0
  130. data/app/views/iron/block_definitions/index.html.erb +18 -0
  131. data/app/views/iron/block_definitions/new.html.erb +8 -0
  132. data/app/views/iron/block_definitions/show.html.erb +34 -0
  133. data/app/views/iron/blocks/new.turbo_stream.erb +6 -0
  134. data/app/views/iron/content_types/_content_type.html.erb +21 -0
  135. data/app/views/iron/content_types/_form.html.erb +137 -0
  136. data/app/views/iron/content_types/edit.html.erb +8 -0
  137. data/app/views/iron/content_types/index.html.erb +28 -0
  138. data/app/views/iron/content_types/new.html.erb +8 -0
  139. data/app/views/iron/content_types/show.html.erb +49 -0
  140. data/app/views/iron/entries/_entry.html.erb +17 -0
  141. data/app/views/iron/entries/_entry_option.html.erb +19 -0
  142. data/app/views/iron/entries/_form.html.erb +63 -0
  143. data/app/views/iron/entries/edit.html.erb +52 -0
  144. data/app/views/iron/entries/entry.html.erb +21 -0
  145. data/app/views/iron/entries/fields/_block.html.erb +92 -0
  146. data/app/views/iron/entries/fields/_block_list.html.erb +45 -0
  147. data/app/views/iron/entries/fields/_boolean.html.erb +13 -0
  148. data/app/views/iron/entries/fields/_date.html.erb +11 -0
  149. data/app/views/iron/entries/fields/_file.html.erb +60 -0
  150. data/app/views/iron/entries/fields/_number.html.erb +11 -0
  151. data/app/views/iron/entries/fields/_reference.html.erb +12 -0
  152. data/app/views/iron/entries/fields/_reference_item.html.erb +28 -0
  153. data/app/views/iron/entries/fields/_reference_list.html.erb +44 -0
  154. data/app/views/iron/entries/fields/_rich_text_area.html.erb +13 -0
  155. data/app/views/iron/entries/fields/_text_area.html.erb +11 -0
  156. data/app/views/iron/entries/fields/_text_field.html.erb +28 -0
  157. data/app/views/iron/entries/index.html.erb +21 -0
  158. data/app/views/iron/entries/new.html.erb +8 -0
  159. data/app/views/iron/entries/search.html.erb +33 -0
  160. data/app/views/iron/field_definitions/_field_definition.html.erb +37 -0
  161. data/app/views/iron/field_definitions/block/_form.html.erb +16 -0
  162. data/app/views/iron/field_definitions/block_list/_form.html.erb +13 -0
  163. data/app/views/iron/field_definitions/boolean/_form.html.erb +2 -0
  164. data/app/views/iron/field_definitions/date/_form.html.erb +2 -0
  165. data/app/views/iron/field_definitions/edit.html.erb +16 -0
  166. data/app/views/iron/field_definitions/file/_form.html.erb +40 -0
  167. data/app/views/iron/field_definitions/index.html.erb +14 -0
  168. data/app/views/iron/field_definitions/layouts/_form.html.erb +47 -0
  169. data/app/views/iron/field_definitions/new.html.erb +22 -0
  170. data/app/views/iron/field_definitions/number/_form.html.erb +2 -0
  171. data/app/views/iron/field_definitions/reference/_form.html.erb +13 -0
  172. data/app/views/iron/field_definitions/reference_list/_form.html.erb +13 -0
  173. data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +2 -0
  174. data/app/views/iron/field_definitions/text_area/_form.html.erb +2 -0
  175. data/app/views/iron/field_definitions/text_field/_form.html.erb +17 -0
  176. data/app/views/iron/first_runs/show.html.erb +44 -0
  177. data/app/views/iron/home/show.html.erb +2 -0
  178. data/app/views/iron/icons/index.html.erb +21 -0
  179. data/app/views/iron/locales/_form.html.erb +24 -0
  180. data/app/views/iron/locales/_locale.html.erb +15 -0
  181. data/app/views/iron/locales/edit.html.erb +8 -0
  182. data/app/views/iron/locales/index.html.erb +12 -0
  183. data/app/views/iron/locales/new.html.erb +8 -0
  184. data/app/views/iron/passwords/edit.html.erb +7 -0
  185. data/app/views/iron/passwords/new.html.erb +26 -0
  186. data/app/views/iron/passwords_mailer/reset.html.erb +4 -0
  187. data/app/views/iron/passwords_mailer/reset.text.erb +2 -0
  188. data/app/views/iron/published_pages/show.html.erb +10 -0
  189. data/app/views/iron/references/new.turbo_stream.erb +6 -0
  190. data/app/views/iron/schemas/new.html.erb +57 -0
  191. data/app/views/iron/sessions/new.html.erb +36 -0
  192. data/app/views/iron/settings/show.html.erb +38 -0
  193. data/app/views/iron/shared/_icon_picker.html.erb +60 -0
  194. data/app/views/iron/users/_form.html.erb +21 -0
  195. data/app/views/iron/users/_user.html.erb +27 -0
  196. data/app/views/iron/users/edit.html.erb +12 -0
  197. data/app/views/iron/users/index.html.erb +21 -0
  198. data/app/views/iron/users/new.html.erb +29 -0
  199. data/app/views/iron/users/show.html.erb +21 -0
  200. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  201. data/app/views/layouts/iron/_sidebar.html.erb +83 -0
  202. data/app/views/layouts/iron/_sidebar_item.html.erb +14 -0
  203. data/app/views/layouts/iron/_toast.html.erb +29 -0
  204. data/app/views/layouts/iron/application.html.erb +46 -0
  205. data/app/views/layouts/iron/authentication.html.erb +41 -0
  206. data/config/importmap.rb +9 -0
  207. data/config/routes.rb +40 -0
  208. data/db/migrate/20250422131656_initial_iron_schema.rb +105 -0
  209. data/db/migrate/20250427100754_create_iron_accounts.rb +10 -0
  210. data/db/migrate/20250427104020_add_role_to_iron_users.rb +5 -0
  211. data/db/migrate/20250428094923_add_default_iron_locale_to_iron_account.rb +5 -0
  212. data/db/migrate/20250504144731_create_join_table_content_types_field_definitions.rb +25 -0
  213. data/db/migrate/20250505090716_add_title_field_definition_to_iron_content_types.rb +5 -0
  214. data/db/migrate/20250512210352_create_iron_references.rb +11 -0
  215. data/db/migrate/20250524220355_add_referenced_entry_id_to_iron_fields.rb +5 -0
  216. data/db/migrate/20250529211703_add_metadata_to_iron_field_definitions.rb +5 -0
  217. data/db/migrate/20250529211704_add_value_boolean_to_iron_fields.rb +5 -0
  218. data/db/migrate/20250531172837_add_type_to_iron_content_types.rb +5 -0
  219. data/db/migrate/20250601080146_add_icon_to_iron_content_types.rb +5 -0
  220. data/db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb +7 -0
  221. data/db/migrate/20250609091813_add_route_to_iron_entries.rb +6 -0
  222. data/lib/generators/iron/pages/pages_generator.rb +34 -0
  223. data/lib/generators/iron/pages/templates/pages_controller.rb +14 -0
  224. data/lib/generators/iron/pages/templates/show.html.erb +9 -0
  225. data/lib/generators/iron/template/template_generator.rb +35 -0
  226. data/lib/generators/iron/template/templates/content_type.html.erb +5 -0
  227. data/lib/iron/cva.rb +69 -0
  228. data/lib/iron/engine.rb +42 -0
  229. data/lib/iron/image_analyzer.rb +26 -0
  230. data/lib/iron/lexorank/exceptions.rb +4 -0
  231. data/lib/iron/lexorank/rankable.rb +185 -0
  232. data/lib/iron/lexorank/rebalance_rank_job.rb +10 -0
  233. data/lib/iron/lexorank/utils.rb +77 -0
  234. data/lib/iron/lexorank.rb +7 -0
  235. data/lib/iron/routing.rb +66 -0
  236. data/lib/iron/sdk.rb +19 -0
  237. data/lib/iron/version.rb +3 -0
  238. data/lib/iron.rb +8 -0
  239. data/lib/puma/plugin/iron_tailwindcss.rb +78 -0
  240. metadata +458 -0
@@ -0,0 +1,43 @@
1
+ module Iron
2
+ class FormBuilder < ActionView::Helpers::FormBuilder
3
+ INPUT_FIELD_TYPES = %i[
4
+ text_field
5
+ password_field
6
+ email_field
7
+ number_field
8
+ tel_field
9
+ url_field
10
+ search_field
11
+ date_field
12
+ time_field
13
+ datetime_field
14
+ datetime_local_field
15
+ month_field
16
+ week_field
17
+ color_field
18
+ ].freeze
19
+
20
+ INPUT_FIELD_TYPES.each do |field_type|
21
+ define_method field_type do |method, options = {}|
22
+ if object&.errors&.[](method).present?
23
+ options[:class] = @template.tw(options[:class], "is-invalid")
24
+ end
25
+
26
+ super(method, options)
27
+ end
28
+ end
29
+
30
+ def error_for(method)
31
+ if object&.errors&.[](method)&.any?
32
+ @template.content_tag(:div, class: "text-red-500 text-sm font-medium mt-1") do
33
+ object.errors[method].join(", ")
34
+ end
35
+ end
36
+ end
37
+
38
+ def label(method, text = nil, options = {}, &block)
39
+ options[:class] = @template.tw("text-sm/6 font-medium", options[:class])
40
+ super(method, text, options, &block)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ module Iron
2
+ module IconsHelper
3
+ SPRITE_PATH = "iron/icons/sprite.svg"
4
+
5
+ def available_icons
6
+ IconCatalog.all
7
+ end
8
+
9
+ # Examples
10
+ # icon "heart" # => 24 px outline heart
11
+ # icon "trash", class: "text-red-600 hover:text-red-700"
12
+ #
13
+ def icon(name, **attributes)
14
+ css_class = attributes.delete(:class)
15
+ data_attrs = { slot: "icon" }.merge!(attributes.delete(:data) || {})
16
+
17
+ tag.svg **attributes.merge(
18
+ class: tw(
19
+ "stroke-current size-5 fill-none stroke-2 [stroke-linecap:round] [stroke-linejoin:round]",
20
+ css_class
21
+ ),
22
+ data: data_attrs,
23
+ viewBox: "0 0 24 24",
24
+ aria_hidden: "true"
25
+ ) do
26
+ tag.use href: "#{image_path(SPRITE_PATH)}##{name}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,50 @@
1
+ module Iron
2
+ module ImageHelper
3
+ IMAGE_BREAKPOINTS = [ 150, 320, 640, 1024, 1920 ].freeze
4
+
5
+ def iron_image_tag(attachment, sizes: "100vw", loading: "lazy", blur: true, **options)
6
+ return "" unless attachment&.attached?
7
+ return image_tag(attachment, **options) unless attachment.blob.variable?
8
+
9
+ image_options = options.merge(
10
+ srcset: generate_srcset(attachment),
11
+ sizes:,
12
+ loading:,
13
+ width: attachment.blob.metadata["width"],
14
+ height: attachment.blob.metadata["height"]
15
+ ).compact
16
+
17
+ if blur && attachment.blob.metadata["blur_data_uri"].present?
18
+ blur_style = "background-image: url('#{attachment.blob.metadata["blur_data_uri"]}'); background-size: cover; background-position: center;"
19
+ image_options[:style] = "#{blur_style} #{options[:style]}".strip
20
+ end
21
+
22
+ image_tag attachment.variant(resize_to_limit: [ IMAGE_BREAKPOINTS.third, nil ]), **image_options
23
+ end
24
+
25
+ def iron_picture_tag(attachment, sizes: "100vw", loading: "lazy", blur: true, **options)
26
+ return "" unless attachment&.attached?
27
+ return image_tag(attachment, **options) unless attachment.blob.variable?
28
+
29
+ content_tag :picture do
30
+ concat source_tag(attachment, :avif, sizes)
31
+ concat source_tag(attachment, :webp, sizes)
32
+ concat iron_image_tag(attachment, sizes: sizes, loading: loading, blur: blur, **options)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def source_tag(attachment, format, sizes)
39
+ tag.source type: "image/#{format}", srcset: generate_srcset(attachment, format:), sizes:
40
+ end
41
+
42
+ def generate_srcset(attachment, format: nil)
43
+ IMAGE_BREAKPOINTS.map { |w| "#{variant_url(attachment, w, format)} #{w}w" }.join(", ")
44
+ end
45
+
46
+ def variant_url(attachment, width, format = nil)
47
+ url_for(attachment.variant(resize_to_limit: [ width, nil ], format: format))
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,6 @@
1
+ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2
+ import "@hotwired/turbo-rails";
3
+ import "controllers";
4
+
5
+ import "trix"
6
+ import "@rails/actiontext"
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+
3
+ const application = Application.start();
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false;
7
+ window.Stimulus = application;
8
+
9
+ export { application };
@@ -0,0 +1,11 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ remove() {
5
+ this.element.remove();
6
+ }
7
+
8
+ focus() {
9
+ this.element.focus();
10
+ }
11
+ }
@@ -0,0 +1,18 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class FileUploadController extends Controller {
4
+ static targets = ["preview", "placeholder"];
5
+
6
+ preview(event) {
7
+ const file = event.target.files[0];
8
+ if (file) {
9
+ const previewUrl = URL.createObjectURL(file);
10
+ this.previewTarget.src = previewUrl;
11
+ if (this.hasPlaceholderTarget) {
12
+ this.placeholderTarget.classList.add("hidden");
13
+ }
14
+ this.previewTarget.classList.remove("hidden");
15
+ }
16
+ }
17
+ }
18
+
@@ -0,0 +1,7 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ submit() {
5
+ this.element.requestSubmit();
6
+ }
7
+ }
@@ -0,0 +1,27 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["source", "destination"];
5
+ static values = { automatic: { type: Boolean, default: true } };
6
+
7
+ handleize() {
8
+ if (this.automaticValue) {
9
+ this.destinationTarget.value = handleize(this.sourceTarget.value);
10
+ }
11
+ }
12
+
13
+ edit(event) {
14
+ this.automaticValue = false;
15
+ const value = event.target.value;
16
+ if (value !== handleize(value)) {
17
+ this.destinationTarget.value = handleize(value);
18
+ }
19
+ }
20
+ }
21
+
22
+ function handleize(str) {
23
+ return str
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9\s_]/g, "")
26
+ .replace(/\s+/g, "_");
27
+ }
@@ -0,0 +1,48 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "preview", "label", "popover", "icon"]
5
+
6
+ connect() {
7
+ this.updateSelectedState()
8
+ }
9
+
10
+ select(event) {
11
+ const iconName = event.currentTarget.dataset.iconName
12
+
13
+ this.inputTarget.value = iconName
14
+ this.previewTarget.innerHTML = event.currentTarget.innerHTML
15
+ this.labelTarget.textContent = iconName
16
+ this.updateSelectedState()
17
+ this.popoverTarget.hidePopover()
18
+ this.inputTarget.dispatchEvent(new Event('change', { bubbles: true }))
19
+ }
20
+
21
+ iconTargetConnected(target) {
22
+ const currentValue = this.inputTarget.value
23
+ if (target.dataset.iconName === currentValue) {
24
+ target.dataset.ui = "selected"
25
+ }
26
+ }
27
+
28
+ updateSelectedState() {
29
+ const currentValue = this.inputTarget.value
30
+ this.iconTargets.forEach(icon => {
31
+ if (icon.dataset.iconName === currentValue) {
32
+ icon.dataset.ui = "selected"
33
+ } else {
34
+ delete icon.dataset.ui
35
+ }
36
+ })
37
+ }
38
+
39
+ filter(event) {
40
+ const searchTerm = event.target.value.toLowerCase()
41
+
42
+ this.iconTargets.forEach(iconButton => {
43
+ const iconName = iconButton.dataset.iconName
44
+ const matches = iconName.includes(searchTerm)
45
+ iconButton.style.display = matches ? '' : 'none'
46
+ })
47
+ }
48
+ }
@@ -0,0 +1,4 @@
1
+ // Import and register all your controllers from the importmap via controllers/**/*_controller
2
+ import { application } from "controllers/application";
3
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
4
+ eagerLoadControllersFrom("controllers", application);
@@ -0,0 +1,125 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import { Lexorank } from "lib/lexorank";
3
+
4
+ export default class extends Controller {
5
+ static targets = ["item", "handle", "rank"];
6
+
7
+ handleTargetConnected(handle) {
8
+ handle.addEventListener("pointerdown", this.#onHandleTapStart);
9
+ handle.addEventListener("pointerup", this.#onHandleTapEnd)
10
+ }
11
+
12
+ handleTargetDisconnected(handle) {
13
+ handle.removeEventListener("pointerdown", this.#onHandleTapStart);
14
+ handle.removeEventListener("pointerup", this.#onHandleTapEnd)
15
+ }
16
+
17
+ rankTargetConnected(rank) {
18
+ if (!rank.value) {
19
+ const item = this.itemTargets.find(i => i.contains(rank));
20
+ this.updateRank(item);
21
+ }
22
+ }
23
+
24
+ onDragStart = (event) => {
25
+ this.draggedEl = event.currentTarget;
26
+ event.dataTransfer.effectAllowed = "move";
27
+ event.dataTransfer.setData("text/plain", "");
28
+ requestAnimationFrame(() => {
29
+ this.draggedEl.classList.add("dragging");
30
+ });
31
+ event.stopPropagation();
32
+ }
33
+
34
+ onDragEnd = () => {
35
+ this.draggedEl.classList.remove("dragging");
36
+ this.updateRank(this.draggedEl);
37
+ this.draggedEl = null;
38
+
39
+ this.#disableDraggableBehaviour();
40
+ }
41
+
42
+ onDragOver = (event) => {
43
+ event.preventDefault();
44
+ const overIndex = this.itemTargets.findIndex(t => t.contains(event.target));
45
+ const overTarget = this.itemTargets[overIndex];
46
+ if (!overTarget || overTarget === this.draggedEl) return;
47
+
48
+ const firstRects = new Map(this.itemTargets.map(el => [el, el.getBoundingClientRect()]));
49
+
50
+ const shouldInsertBefore = this.draggedEl.getBoundingClientRect().top > overTarget.getBoundingClientRect().top;
51
+ this.element.insertBefore(this.draggedEl, shouldInsertBefore ? overTarget : this.itemTargets[overIndex + 1] ?? null);
52
+
53
+ this.itemTargets.forEach(el => {
54
+ const dy = firstRects.get(el).top - el.getBoundingClientRect().top;
55
+ if (dy) {
56
+ el.style.transition = "none";
57
+ el.style.transform = `translateY(${dy}px)`;
58
+ requestAnimationFrame(() => {
59
+ el.style.transition = "transform 150ms ease";
60
+ el.style.transform = "";
61
+ });
62
+ }
63
+ });
64
+ }
65
+
66
+ updateRank(item) {
67
+ const idx = this.itemTargets.indexOf(item);
68
+ const prevRank = idx > 0 ? this.#rankFor(this.itemTargets[idx - 1]) : undefined;
69
+ const nextRank = idx < this.itemTargets.length - 1 ? this.#rankFor(this.itemTargets[idx + 1]) : undefined;
70
+ const newRank = Lexorank.between(prevRank, nextRank);
71
+ this.#setRank(item, newRank);
72
+ }
73
+
74
+ // Private
75
+
76
+ #onHandleTapStart = () => {
77
+ this.#enableDraggableBehaviour();
78
+ }
79
+
80
+ #onHandleTapEnd = () => {
81
+ this.#disableDraggableBehaviour();
82
+ }
83
+
84
+ #enableDraggableBehaviour = () => {
85
+ this.element.addEventListener("dragover", this.onDragOver);
86
+ this.itemTargets.forEach(this.#enableItemDraggableBehaviour);
87
+ }
88
+
89
+ #disableDraggableBehaviour = () => {
90
+ this.element.removeEventListener("dragover", this.onDragOver);
91
+ this.itemTargets.forEach(this.#disableItemDraggableBehaviour);
92
+ };
93
+
94
+ #enableItemDraggableBehaviour = (item) => {
95
+ item.setAttribute("draggable", "true");
96
+ item.addEventListener("dragstart", this.onDragStart);
97
+ item.addEventListener("dragend", this.onDragEnd);
98
+ }
99
+
100
+ #disableItemDraggableBehaviour = (item) => {
101
+ item.removeAttribute("draggable");
102
+ item.removeEventListener("dragstart", this.onDragStart);
103
+ item.removeEventListener("dragend", this.onDragEnd);
104
+ }
105
+
106
+ #rankFor(item) {
107
+ return this.#findRankInputForItem(item)?.value;
108
+ }
109
+
110
+ #setRank(item, value) {
111
+ const input = this.#findRankInputForItem(item);
112
+ if (input && input.value != value) {
113
+ input.value = value;
114
+ input.dispatchEvent(
115
+ new Event("change", { bubbles: true, cancelable: true })
116
+ );
117
+ }
118
+ }
119
+
120
+ #findRankInputForItem(item) {
121
+ const input = this.rankTargets.find(r => item.contains(r));
122
+ if (input == null) console.warn("Item is missing a rank input target", item);
123
+ return input;
124
+ }
125
+ }
@@ -0,0 +1,14 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static values = { selector: String };
5
+
6
+ toggle(event) {
7
+ const isChecked = event.target.checked;
8
+ const targets = document.querySelectorAll(this.selectorValue);
9
+
10
+ targets.forEach(target => {
11
+ target.dataset.ui = isChecked ? "visible" : "hidden";
12
+ });
13
+ }
14
+ }
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ this.element.addEventListener("turbo:before-morph-attribute", this.#preventMorphing);
6
+ this.element.addEventListener("turbo:before-morph-element", this.#preventMorphing);
7
+ }
8
+
9
+ // Private
10
+
11
+ #preventMorphing = (event) => {
12
+ const { target } = event;
13
+
14
+ if (target.tagName == "TRIX-EDITOR" || target.tagName == "TRIX-TOOLBAR") {
15
+ event.preventDefault();
16
+ }
17
+ }
18
+ }
19
+
@@ -0,0 +1,51 @@
1
+ const MIN_RANK_VALUE = "0";
2
+ const MAX_RANK_VALUE = "z";
3
+
4
+ export class Lexorank {
5
+ static between(before, after) {
6
+ const _before = before || MIN_RANK_VALUE;
7
+ const _after = after || MAX_RANK_VALUE;
8
+ let result = '';
9
+ let index = 0;
10
+ let found = false;
11
+
12
+ while (!found) {
13
+ const prev_char = _before[index] || MIN_RANK_VALUE;
14
+ const after_char = _after[index] || MAX_RANK_VALUE;
15
+ if (prev_char === after_char) {
16
+ result += prev_char;
17
+ index++;
18
+ } else {
19
+ const mid_char = character_between(prev_char, after_char);
20
+ if (mid_char === prev_char || mid_char === after_char) {
21
+ result += prev_char;
22
+ index++;
23
+ } else {
24
+ result += mid_char;
25
+ found = true;
26
+ }
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {String} prev A single character
35
+ * @param {String} after A single character
36
+ * @returns {String} A character that sits between prev and after
37
+ */
38
+ function character_between(prev, after) {
39
+ let middle_ascii = Math.round((ascii(prev) + ascii(after)) / 2);
40
+ return String.fromCharCode(middle_ascii);
41
+ }
42
+
43
+ /**
44
+ * @param {String} char A single character
45
+ * @returns {Number} The ascii value of char
46
+ */
47
+ function ascii(char) {
48
+ return char.charCodeAt(0);
49
+ }
50
+
51
+ window.Lexorank = Lexorank;
@@ -0,0 +1,4 @@
1
+ module Iron
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module Iron
2
+ class GenerateEntryRoutesJob < ApplicationJob
3
+ queue_as :default
4
+
5
+ def perform(content_type = nil)
6
+ if content_type
7
+ content_type.entries.where(route: nil).find_each(&:save) if content_type.web_publishing_enabled?
8
+ else
9
+ ContentType.web_published.find_each do |ct|
10
+ ct.entries.where(route: nil).find_each(&:save)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ module Iron
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class PasswordsMailer < ApplicationMailer
2
+ def reset(user)
3
+ @user = user
4
+ mail subject: "Reset your password", to: user.email_address
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ module Iron
2
+ module CsvSerializable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def to_csv
7
+ CSV.generate do |csv|
8
+ csv << csv_headers
9
+ csv_scope.find_each do |record|
10
+ csv << record.to_csv_row
11
+ end
12
+ end
13
+ end
14
+
15
+ def csv_headers
16
+ raise NotImplementedError, "#{name} must implement csv_headers"
17
+ end
18
+
19
+ def csv_scope
20
+ all
21
+ end
22
+ end
23
+
24
+ def to_csv_row
25
+ raise NotImplementedError, "#{self.class.name} must implement to_csv_row"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ module Iron::Account::Joinable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_create { self.join_code = generate_join_code }
6
+ end
7
+
8
+ def reset_join_code
9
+ update! join_code: generate_join_code
10
+ end
11
+
12
+ private
13
+ def generate_join_code
14
+ SecureRandom.alphanumeric(12).scan(/.{4}/).join("-")
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ module Iron
2
+ class Account < ApplicationRecord
3
+ include Joinable
4
+
5
+ belongs_to :default_locale, class_name: "Iron::Locale"
6
+
7
+ def self.clear_schema!
8
+ FieldDefinition.destroy_all
9
+ ContentType.destroy_all
10
+ BlockDefinition.destroy_all
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module Iron
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ module Iron
2
+ module BlockDefinition::Portable
3
+ extend ActiveSupport::Concern
4
+ include ::Iron::CsvSerializable
5
+
6
+ class_methods do
7
+ def csv_headers
8
+ %w[handle name description]
9
+ end
10
+ end
11
+
12
+ def to_csv_row
13
+ [
14
+ handle,
15
+ name,
16
+ description
17
+ ]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ module Iron
2
+ class BlockDefinition < ApplicationRecord
3
+ include Portable
4
+
5
+ has_many :field_definitions, -> { ranked }, as: :schemable, dependent: :destroy
6
+
7
+ validates :name, presence: true
8
+ validates :handle, presence: true
9
+ end
10
+ end