gaco_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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +48 -0
  3. data/Rakefile +10 -0
  4. data/app/assets/builds/gaco_cms.css +18257 -0
  5. data/app/assets/builds/gaco_cms.js +43235 -0
  6. data/app/assets/builds/gaco_cms.js.map +7 -0
  7. data/app/assets/builds/gaco_cms_front.css +18237 -0
  8. data/app/assets/builds/gaco_cms_front.js +10722 -0
  9. data/app/assets/builds/gaco_cms_front.js.map +7 -0
  10. data/app/assets/javascripts/gaco_cms/base.js +24 -0
  11. data/app/assets/javascripts/gaco_cms/controllers/confirm_controller.ts +20 -0
  12. data/app/assets/javascripts/gaco_cms/controllers/deletable_row_controller.ts +46 -0
  13. data/app/assets/javascripts/gaco_cms/controllers/editor_controller.ts +58 -0
  14. data/app/assets/javascripts/gaco_cms/controllers/file_input_controller.ts +82 -0
  15. data/app/assets/javascripts/gaco_cms/controllers/modal_controller.ts +73 -0
  16. data/app/assets/javascripts/gaco_cms/controllers/remote_content_controller.ts +27 -0
  17. data/app/assets/javascripts/gaco_cms/controllers/repeatable_field_controller.ts +26 -0
  18. data/app/assets/javascripts/gaco_cms/controllers/sortable_controller.ts +27 -0
  19. data/app/assets/javascripts/gaco_cms/controllers/toggle_field_controller.ts +37 -0
  20. data/app/assets/javascripts/gaco_cms/controllers/translatable_controller.ts +86 -0
  21. data/app/assets/javascripts/gaco_cms/lib/progress_bar.ts +7 -0
  22. data/app/assets/javascripts/gaco_cms/lib/request.ts +21 -0
  23. data/app/assets/javascripts/gaco_cms/lib/turbo_request.ts +61 -0
  24. data/app/assets/javascripts/gaco_cms/stimulus.ts +9 -0
  25. data/app/assets/javascripts/gaco_cms.js +7 -0
  26. data/app/assets/javascripts/gaco_cms_front.js +3 -0
  27. data/app/assets/stylesheets/gaco_cms/bootstrap_custom.scss +15 -0
  28. data/app/assets/stylesheets/gaco_cms/shared.scss +22 -0
  29. data/app/assets/stylesheets/gaco_cms.css.scss +8 -0
  30. data/app/assets/stylesheets/gaco_cms_front.css.scss +6 -0
  31. data/app/controllers/concerns/gaco_cms/turbo_concern.rb +52 -0
  32. data/app/controllers/gaco_cms/admin/base_controller.rb +20 -0
  33. data/app/controllers/gaco_cms/admin/field_groups_controller.rb +87 -0
  34. data/app/controllers/gaco_cms/admin/field_groups_manager_controller.rb +66 -0
  35. data/app/controllers/gaco_cms/admin/field_groups_renderer_controller.rb +33 -0
  36. data/app/controllers/gaco_cms/admin/fields_controller.rb +20 -0
  37. data/app/controllers/gaco_cms/admin/page_types_controller.rb +56 -0
  38. data/app/controllers/gaco_cms/admin/pages_controller.rb +70 -0
  39. data/app/controllers/gaco_cms/admin/themes_controller.rb +55 -0
  40. data/app/controllers/gaco_cms/admin_controller.rb +47 -0
  41. data/app/controllers/gaco_cms/front_controller.rb +39 -0
  42. data/app/helpers/gaco_cms/application_helper.rb +26 -0
  43. data/app/jobs/gaco_cms/application_job.rb +6 -0
  44. data/app/mailers/gaco_cms/application_mailer.rb +8 -0
  45. data/app/models/concerns/gaco_cms/fields_assignable.rb +40 -0
  46. data/app/models/concerns/gaco_cms/union_scope.rb +14 -0
  47. data/app/models/gaco_cms/application_record.rb +25 -0
  48. data/app/models/gaco_cms/draft_record.rb +16 -0
  49. data/app/models/gaco_cms/field.rb +78 -0
  50. data/app/models/gaco_cms/field_group.rb +55 -0
  51. data/app/models/gaco_cms/field_value.rb +77 -0
  52. data/app/models/gaco_cms/media_file.rb +22 -0
  53. data/app/models/gaco_cms/page.rb +57 -0
  54. data/app/models/gaco_cms/page_type.rb +38 -0
  55. data/app/models/gaco_cms/theme.rb +28 -0
  56. data/app/services/gaco_cms/application_service.rb +33 -0
  57. data/app/services/gaco_cms/shortcode_parser.rb +217 -0
  58. data/app/services/gaco_cms/theme_content_generator.rb +55 -0
  59. data/app/views/gaco_cms/admin/base/index.html.haml +4 -0
  60. data/app/views/gaco_cms/admin/field_groups_manager/_field.html.haml +53 -0
  61. data/app/views/gaco_cms/admin/field_groups_manager/_group.html.haml +54 -0
  62. data/app/views/gaco_cms/admin/field_groups_manager/index.html.haml +14 -0
  63. data/app/views/gaco_cms/admin/field_groups_renderer/_field.html.haml +31 -0
  64. data/app/views/gaco_cms/admin/field_groups_renderer/_fields_frame.html.haml +16 -0
  65. data/app/views/gaco_cms/admin/field_groups_renderer/_group.html.haml +24 -0
  66. data/app/views/gaco_cms/admin/field_groups_renderer/default_value/_page.html.haml +3 -0
  67. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_editor.html.haml +3 -0
  68. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_file.html.haml +4 -0
  69. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_page.html.haml +3 -0
  70. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_text_area.html.haml +3 -0
  71. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_text_field.html.haml +3 -0
  72. data/app/views/gaco_cms/admin/field_groups_renderer/index.html.haml +20 -0
  73. data/app/views/gaco_cms/admin/page_types/form.html.haml +27 -0
  74. data/app/views/gaco_cms/admin/page_types/index.html.haml +27 -0
  75. data/app/views/gaco_cms/admin/pages/edit.html.haml +57 -0
  76. data/app/views/gaco_cms/admin/pages/index.html.haml +23 -0
  77. data/app/views/gaco_cms/admin/pages/new.html.haml +16 -0
  78. data/app/views/gaco_cms/admin/themes/edit.html.haml +36 -0
  79. data/app/views/gaco_cms/admin/themes/index.html.haml +24 -0
  80. data/app/views/gaco_cms/admin/themes/new.html.haml +21 -0
  81. data/app/views/layouts/gaco_cms/_breadcrumb.html.haml +7 -0
  82. data/app/views/layouts/gaco_cms/_flash_messages.html.haml +13 -0
  83. data/app/views/layouts/gaco_cms/admin.haml +55 -0
  84. data/config/routes.rb +26 -0
  85. data/db/migrate/20230202133738_create_cms_pages.rb +16 -0
  86. data/db/migrate/20230202134840_create_cms_field_groups.rb +17 -0
  87. data/db/migrate/20230202135314_create_cms_fields.rb +21 -0
  88. data/db/migrate/20230202135739_create_cms_field_values.rb +16 -0
  89. data/db/migrate/20230202181744_create_cms_media_files.rb +10 -0
  90. data/db/migrate/20230206231547_create_cms_page_types.rb +23 -0
  91. data/db/migrate/20230209090008_create_cms_themes.rb +19 -0
  92. data/lib/gaco_cms/config.rb +19 -0
  93. data/lib/gaco_cms/engine.rb +31 -0
  94. data/lib/gaco_cms/version.rb +5 -0
  95. data/lib/gaco_cms.rb +18 -0
  96. data/lib/generators/gaco_cms/install_generator.rb +16 -0
  97. data/lib/generators/gaco_cms/install_template/gaco_cms.rb +11 -0
  98. data/lib/generators/gaco_cms/install_template/themes/default/index.html.haml +2 -0
  99. data/lib/generators/gaco_cms/install_template/themes/default/layouts/application.html.haml +34 -0
  100. data/lib/generators/gaco_cms/install_template/themes/default/page.html.haml +2 -0
  101. data/lib/tasks/auto_annotate_models.rake +59 -0
  102. data/lib/tasks/gaco_cms_tasks.rake +5 -0
  103. metadata +174 -0
@@ -0,0 +1,24 @@
1
+ import application from './stimulus';
2
+
3
+ import TranslatableController from './controllers/translatable_controller';
4
+ import EditorController from './controllers/editor_controller';
5
+ import DeletableRowController from './controllers/deletable_row_controller';
6
+ import FormConfirmController from "./controllers/confirm_controller";
7
+ import RepeatableFieldController from './controllers/repeatable_field_controller';
8
+ import FileInputController from './controllers/file_input_controller';
9
+ import RemoteContentController from './controllers/remote_content_controller';
10
+ import ToggleFieldController from './controllers/toggle_field_controller';
11
+ import SortableController from './controllers/sortable_controller';
12
+ import ModalController from './controllers/modal_controller';
13
+
14
+ application.register('gaco-cms-translatable', TranslatableController);
15
+ application.register('gaco-cms-editor', EditorController);
16
+ application.register('gaco-cms-deletable_row', DeletableRowController);
17
+ application.register('gaco-cms-repeatable-field', RepeatableFieldController);
18
+ application.register('gaco-cms-file-input', FileInputController);
19
+ application.register('gaco-cms-remote-content', RemoteContentController);
20
+ application.register('gaco-cms-toggle-field', ToggleFieldController);
21
+ application.register('gaco-cms-sortable', SortableController);
22
+ application.register('gaco-cms-modal', ModalController);
23
+ application.register('form-confirm', FormConfirmController);
24
+
@@ -0,0 +1,20 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // Sample: = form_for @appointment, url: url_for(action: :destroy), method: :delete, html: { data: { controller: 'form-confirm', 'confirm' => 'Are you sure you want to cancel this appointment?' } } do |f|
4
+ export default class extends Controller {
5
+ static values = { message: String };
6
+ declare messageValue: string;
7
+ declare element: HTMLFormElement|HTMLElement;
8
+
9
+ initialize() {
10
+ const action = this.element.tagName == 'FORM' ? 'submit' : 'click';
11
+ this.element.addEventListener(action, this.confirm.bind(this), false);
12
+ }
13
+
14
+ confirm(event) {
15
+ if (!(window.confirm(this.messageValue || this.element.getAttribute('data-confirm')))) {
16
+ event.preventDefault();
17
+ event.stopImmediatePropagation();
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,46 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // Sample: = f.check_box :_destroy, 'data-controller': 'gaco-cms-deletable_row', 'data-hide-closest': '.list-group-item', 'data-hideonly': value.id.present?
4
+ // data-hideonly => [boolean] if data-hideonly is empty or false, just hide the data-hide-closest panel, else, it is removed
5
+ // data-hide-closest => [css selector] the panel to be hide/removed
6
+ // data-propagate => [css selector] elements to be marked as checked
7
+
8
+ export default class extends Controller {
9
+ declare element: HTMLInputElement;
10
+ declare panelToHide: HTMLElement;
11
+ declare isSaved: boolean;
12
+
13
+ initialize() {
14
+ this.element.style.display = 'none';
15
+ this.element.insertAdjacentHTML('afterend', this.deleteBtn());
16
+ const hideTarget = this.element.getAttribute('data-hide-closest');
17
+ this.isSaved = (this.element.getAttribute('data-hideonly') || 'false') != 'false';
18
+ if (hideTarget) this.panelToHide = this.element.closest(hideTarget);
19
+ this.bindEvent();
20
+ }
21
+
22
+ deleteBtn() {
23
+ return `<button type="button" class="btn btn-danger btn-sm del-btn">
24
+ <i class="fa fa-trash"></i>
25
+ </button>`;
26
+ }
27
+
28
+ bindEvent() {
29
+ const that = this;
30
+ this.element.nextElementSibling.addEventListener('click', () => {
31
+ if (!that.isSaved || confirm('Are you sure?')) {
32
+ that.element.checked = true;
33
+ if (that.panelToHide) that.hidePanel();
34
+ }
35
+ }, false);
36
+ }
37
+
38
+ hidePanel() {
39
+ if (this.isSaved) {
40
+ this.panelToHide.style.display = 'none';
41
+ const propagated = this.element.getAttribute('data-propagate');
42
+ if (propagated)
43
+ this.panelToHide.querySelectorAll<HTMLInputElement>(propagated).forEach((ele) => ele.checked = true);
44
+ } else this.panelToHide.remove();
45
+ }
46
+ }
@@ -0,0 +1,58 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // Sample: = f.text_area :template, class: 'form-control', data: { controller: 'gaco-cms-editor', height: '400' }
4
+ export default class extends Controller {
5
+ declare element: HTMLTextAreaElement|HTMLInputElement;
6
+
7
+ initialize() {
8
+ const attrController = this.element.getAttribute('data-controller');
9
+ const isTranslatable = attrController.includes('gaco-cms-translatable');
10
+ if (!isTranslatable) this.buildEditor();
11
+ }
12
+
13
+ // source code: https://www.tiny.cloud/docs/demo/file-picker/
14
+ buildEditor() {
15
+ const that = this;
16
+ tinyMCE.baseURL = "/assets/gaco_cms/tinymce";
17
+ tinymce.init({
18
+ selector: `#${this.element.id}`,
19
+ plugins: 'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media template codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap quickbars emoticons',
20
+ image_title: true,
21
+ automatic_uploads: true,
22
+ images_upload_url: window.gaco_cms_config.upload_path,
23
+ height: this.element.dataset.height || 600,
24
+ content_css: window.gaco_cms_config.editor_css,
25
+ convert_urls: false,
26
+ file_picker_callback: function (cb, value, meta) {
27
+ that.uploadFile(cb, that.calcFormat(meta));
28
+ },
29
+ content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }'
30
+ });
31
+ }
32
+
33
+ calcFormat(meta) {
34
+ if (meta.filetype === 'file') return '*/*';
35
+ if (meta.filetype === 'image') return 'image/*';
36
+ if (meta.filetype === 'media') return 'audio/*,video/*';
37
+ }
38
+
39
+ uploadFile(cb, format) {
40
+ var input = document.createElement('input');
41
+ input.setAttribute('type', 'file');
42
+ input.setAttribute('accept', format);
43
+ input.onchange = function () {
44
+ var file = this.files[0];
45
+ var reader = new FileReader();
46
+ reader.onload = function () {
47
+ var id = 'blobid' + (new Date()).getTime();
48
+ var blobCache = tinymce.activeEditor.editorUpload.blobCache;
49
+ var base64 = reader.result.split(',')[1];
50
+ var blobInfo = blobCache.create(id, file, base64);
51
+ blobCache.add(blobInfo);
52
+ cb(blobInfo.blobUri(), { title: file.name });
53
+ };
54
+ reader.readAsDataURL(file);
55
+ };
56
+ input.click();
57
+ }
58
+ }
@@ -0,0 +1,82 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { ajaxRequest } from "../lib/request";
3
+
4
+ // converts a text-field into a uploadable field
5
+ // Sample:
6
+ // .photo_panel{ data: { controller: 'gaco-cms-file-input' } }
7
+ // = f.text_field :photo_url, class: 'form-control', accept: 'image/*', data: { 'gaco-cms-file-input-target': 'input' }
8
+ export default class extends Controller {
9
+ declare element: HTMLElement;
10
+ static targets = ['input'];
11
+ declare inputTarget: HTMLInputElement;
12
+
13
+
14
+ initialize() {
15
+ setTimeout(this.parseElement.bind(this), 200); // delay to wait for translatable structure
16
+ }
17
+
18
+ parseElement() {
19
+ this.element.setAttribute('data-file-input-redered', 'true');
20
+ this.element.classList.add('input-group');
21
+ this.element.insertAdjacentHTML('beforeend', this.tpl());
22
+ this.bindPreview();
23
+ this.dispatchChange();
24
+ this.bindUploader();
25
+ }
26
+
27
+ tpl() {
28
+ return `
29
+ <button class="btn btn-outline-light btn-upload" type="button">
30
+ <i class="fa fa-upload"></i>
31
+ </button>
32
+ <span class="preview input-group-text p-0"></span>`;
33
+ }
34
+
35
+ bindPreview() {
36
+ const that = this;
37
+ const preview = this.element.querySelector('.preview');
38
+ this.inputTarget.addEventListener('change', () => {
39
+ preview.innerHTML = that.previewTpl(that.inputTarget.value);
40
+ });
41
+ }
42
+
43
+ previewTpl(url) {
44
+ const previewExt = ['png', 'jpg', 'jpeg', 'gif'];
45
+ if (!url) return '';
46
+ if (!previewExt.includes(url.split('.').pop().toLowerCase())) return '';
47
+
48
+ return `
49
+ <a href="${url}" target="_blank">
50
+ <img src="${url}" style="max-width: 50px; max-height: 50px;" />
51
+ </a>
52
+ `;
53
+ }
54
+
55
+ bindUploader() {
56
+ const btn = this.element.querySelector('.btn-upload');
57
+ const that = this;
58
+ btn.addEventListener('click', () => {
59
+ const input = document.createElement('input');
60
+ input.setAttribute('type', 'file');
61
+ input.setAttribute('accept', that.inputTarget.getAttribute('accept'));
62
+ input.onchange = async function () {
63
+ await that.uploadFile(input);
64
+ };
65
+ input.click();
66
+ });
67
+ }
68
+
69
+ async uploadFile(field: HTMLInputElement) {
70
+ const formData = new FormData();
71
+ formData.append('file', field.files[0]);
72
+ const res = await ajaxRequest(window.gaco_cms_config.upload_path, formData, 'POST', 'json');
73
+ if (res) {
74
+ this.inputTarget.value = res.location;
75
+ this.dispatchChange();
76
+ }
77
+ }
78
+
79
+ dispatchChange() {
80
+ this.inputTarget.dispatchEvent(new Event('change'));
81
+ }
82
+ }
@@ -0,0 +1,73 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { Modal } from 'bootstrap';
3
+
4
+ let modalsCounter = 1;
5
+ export default class extends Controller {
6
+ static values = { body: String, title: String, target: String, size: String, selfModal: Boolean };
7
+ declare bodyValue: string;
8
+ declare titleValue: string;
9
+ declare targetValue: string;
10
+ declare sizeValue: string;
11
+ declare selfModalValue: boolean;
12
+ declare element: HTMLLinkElement|HTMLButtonElement;
13
+ declare modalId: string;
14
+
15
+ initialize() {
16
+ const that = this;
17
+ if (this.selfModalValue) {
18
+ this.buildModal();
19
+ this.element.remove();
20
+ } else {
21
+ this.element.addEventListener('click', (e) => {
22
+ e.preventDefault();
23
+ e.stopPropagation();
24
+ that.buildModal();
25
+ });
26
+ }
27
+ }
28
+
29
+ tpl(content) {
30
+ return `
31
+ <div class="modal fade" id="${this.modalId}" aria-hidden="true" aria-labelledby="exampleModalToggleLabel" tabindex="-1">
32
+ <div class="modal-dialog ${this.sizeValue}">
33
+ <div class="modal-content">
34
+ <div class="modal-header">
35
+ <h5 class="modal-title">${this.titleValue || this.element.getAttribute('data-title') || ''}</h5>
36
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
37
+ </div>
38
+ <div class="modal-body">
39
+ ${content}
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ `;
45
+ }
46
+
47
+ calcContent() {
48
+ if (this.selfModalValue) return this.element.innerHTML;
49
+ if (this.bodyValue) return this.bodyValue;
50
+ if (this.targetValue) return document.body.querySelector(this.targetValue).innerHTML;
51
+ if (this.element.tagName == 'A')
52
+ return `<turbo-frame id='turbo-frame-${this.modalId}' src='${this.element.href}'></turbo-frame>`;
53
+
54
+ return '';
55
+ }
56
+
57
+ buildModal() {
58
+ this.modalId = `modal_${modalsCounter}`;
59
+ document.body.insertAdjacentHTML('beforeend', this.tpl(this.calcContent()));
60
+ modalsCounter += 1;
61
+ setTimeout(this.showModal.bind(this), 2);
62
+ }
63
+
64
+ showModal() {
65
+ const modalEle = document.getElementById(this.modalId);
66
+ const modal = new Modal(modalEle);
67
+ modal.show();
68
+ modalEle.addEventListener('close-modal', () => { modal.hide() });
69
+ modalEle.addEventListener('hidden.bs.modal', function (event) {
70
+ modalEle.remove();
71
+ });
72
+ }
73
+ }
@@ -0,0 +1,27 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { ajaxRequest } from "../lib/request";
3
+
4
+ // Sample: %li= link_to(v, url_for(action: :new_field, id: @group, kind: k), class: 'dropdown-item', data: { controller: 'gaco-cms-remote-content', 'gaco-cms-remote-content-target-value': '#fields_list' })
5
+ export default class extends Controller {
6
+ declare element: HTMLLinkElement;
7
+ static values = { target: String };
8
+ declare targetValue: string;
9
+
10
+
11
+ initialize() {
12
+ const that = this;
13
+ this.element.addEventListener('click', (ev) => {
14
+ ev.preventDefault();
15
+ that.loadContent();
16
+ });
17
+ }
18
+
19
+ async loadContent() {
20
+ const res = await ajaxRequest(this.element.href);
21
+ if (!res) return;
22
+
23
+ document.body.querySelector<HTMLElement>(this.targetValue).insertAdjacentHTML('beforeend', res);
24
+ // TODO: if inside modal, scroll modal scroll instead of window
25
+ window.scrollTo(0, document.body.scrollHeight);
26
+ }
27
+ }
@@ -0,0 +1,26 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import { ajaxRequest } from "../lib/request";
3
+
4
+ // Add the ability to load tpl content from server via ajax and add it to listTarget
5
+ // Sample:
6
+ // .fields-list{ 'data-controller': 'gaco-cms-repeatable-field' }
7
+ // = link_to 'My Title' tpl_admin_field_path(id: field, group_no: group_no), class: 'btn btn-sm', 'data-gaco-cms-repeatable-field-target' => 'button'
8
+ // %ul.list-group.list-group-flush{ 'data-gaco-cms-repeatable-field-target': 'list' }
9
+ export default class extends Controller {
10
+ static targets = ['button', 'list'];
11
+ declare element: HTMLElement;
12
+ declare buttonTarget: HTMLLinkElement;
13
+ declare listTarget: HTMLUListElement;
14
+ declare hasButtonTarget: boolean
15
+
16
+ initialize() {
17
+ if (!this.hasButtonTarget) return;
18
+ this.buttonTarget.addEventListener('click', this.loadTpl.bind(this));
19
+ }
20
+
21
+ async loadTpl(event) {
22
+ event.preventDefault();
23
+ const res = await ajaxRequest(this.buttonTarget.href);
24
+ this.listTarget.insertAdjacentHTML('beforeend', res);
25
+ }
26
+ }
@@ -0,0 +1,27 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+ import Sortable from 'sortablejs';
3
+
4
+ // Sample:
5
+ // #fields_list.accordion{ data: { controller: 'gaco-cms-sortable', 'gaco-cms-sortable-handle-value': '.accordion-item .sort-btn', 'gaco-cms-sortable-input-selector-value': '.position-field' } }
6
+ export default class extends Controller {
7
+ declare element: HTMLElement;
8
+ static values = { handle: String, inputSelector: String };
9
+ declare handleValue: string;
10
+ declare inputSelectorValue: string;
11
+
12
+ initialize() {
13
+ new Sortable(this.element, {
14
+ handle: this.handleValue,
15
+ animation: 150,
16
+ onEnd: this.updateFieldValues.bind(this)
17
+ });
18
+ }
19
+
20
+ updateFieldValues() {
21
+ if (!this.inputSelectorValue) return;
22
+
23
+ this.element.querySelectorAll<HTMLInputElement>(this.inputSelectorValue).forEach((input, index) => {
24
+ input.value = `${index + 1}`;
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,37 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // Sample:
4
+ // .fw{ data: { controller: 'gaco-cms-toggle-field', 'gaco-cms-toggle-field-open-value': @model.my_key.present? } }
5
+ // = f.text_field :my_key
6
+ export default class extends Controller {
7
+ declare element: HTMLDivElement;
8
+ static values = { open: Boolean };
9
+ declare openValue: boolean;
10
+
11
+ initialize() {
12
+ if (this.openValue) return;
13
+
14
+ this.element.insertAdjacentHTML('beforebegin', this.editIconTpl());
15
+ this.editBtn().addEventListener('click', this.showElement.bind(this));
16
+ this.element.classList.add('d-none');
17
+ }
18
+
19
+ showElement() {
20
+ this.element.classList.remove('d-none');
21
+ this.element.previousElementSibling.remove();
22
+ }
23
+
24
+ editIconTpl() {
25
+ return `
26
+ <div>
27
+ <button class="btn btn-sm btn-secondary edit-field" type="button">
28
+ <i class="fa fa-pencil"></i>
29
+ </button>
30
+ </div>
31
+ `;
32
+ }
33
+
34
+ editBtn() {
35
+ return this.element.previousElementSibling.querySelector('button');
36
+ }
37
+ }
@@ -0,0 +1,86 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ let fieldsCounter = 0;
4
+ // Sample: = f.text_field :title, value: @page.title_data.to_json, class: 'form-control', required: true, data: { controller: 'gaco-cms-translatable' }
5
+ export default class extends Controller {
6
+ declare element: HTMLTextAreaElement|HTMLInputElement;
7
+ static values = {
8
+ closestClone: String
9
+ }
10
+ declare dataValue: object;
11
+ declare dataName: string;
12
+ declare locales: string[];
13
+ declare currentLoc: string;
14
+ declare closestCloneValue: string;
15
+
16
+ initialize() {
17
+ fieldsCounter += 1;
18
+ this.locales = window.gaco_cms_config.locales;
19
+ this.currentLoc = window.gaco_cms_config.locale;
20
+ try {
21
+ this.dataValue = JSON.parse(this.element.value || '{}');
22
+ } catch {
23
+ this.dataValue = { [this.currentLoc]: this.element.value };
24
+ }
25
+ this.dataName = this.element.name;
26
+ this.elementToHide().insertAdjacentHTML('afterend', this.tpl());
27
+ this.hideElement();
28
+ }
29
+
30
+ hideElement() {
31
+ this.element.name = '';
32
+ this.elementToHide().style.display = 'none';
33
+ }
34
+
35
+ elementToHide(): HTMLElement {
36
+ if (!this.closestCloneValue) return this.element;
37
+
38
+ return this.element.closest<HTMLElement>(this.closestCloneValue);
39
+ }
40
+
41
+ tpl() {
42
+ return `
43
+ <div class="translation-panel">
44
+ <ul class="nav nav-tabs justify-content-end" role="tablist" style="margin-top: -24px">
45
+ ${ this.locales.map((loc) =>
46
+ `<li class="nav-item" role="presentation">
47
+ <button class="nav-link pt-0 pb-0 ${ loc == this.currentLoc ? 'active' : '' }"
48
+ data-bs-target="#${this.localeKey(loc)}-tab" data-bs-toggle="tab" type="button" role="tab"
49
+ aria-controls="${loc}" aria-selected="true" tabindex="-1">${loc}</button>
50
+ </li>` ).join('') }
51
+ </ul>
52
+ <div class="tab-content">
53
+ ${ this.locales.map((loc) =>
54
+ `<div class="tab-pane fade ${ loc == this.currentLoc ? 'show active' : '' }" role="tabpanel" id="${this.localeKey(loc)}-tab">
55
+ ${this.fieldFor(loc)}
56
+ </div>` ).join('') }
57
+ </div>
58
+ </div>
59
+ `;
60
+ }
61
+
62
+ localeKey(locale) {
63
+ return `${this.dataName.replace(/[\W_]+/g, '_')}_${locale}_${fieldsCounter}`;
64
+ }
65
+
66
+ fieldFor(locale) {
67
+ const value = this.dataValue[locale] || '';
68
+ const clone = this.element.cloneNode() as HTMLInputElement;
69
+ const updatedAttr = clone.getAttribute('data-controller').replace('gaco-cms-translatable', '');
70
+ clone.setAttribute('data-controller', updatedAttr);
71
+ clone.setAttribute('value', value);
72
+ clone.removeAttribute('required'); // TODO: make only for the hidden ones
73
+ clone.innerHTML = value;
74
+ clone.classList.add('translation-field');
75
+ clone.name = `${this.dataName}[${locale}]`;
76
+ clone.id = `${this.localeKey(locale)}`;
77
+
78
+ if (this.closestCloneValue) {
79
+ const panel = this.element.closest(this.closestCloneValue).cloneNode(true) as HTMLElement;
80
+ panel.querySelector('input').outerHTML = clone.outerHTML;
81
+ return panel.outerHTML;
82
+ } else {
83
+ return clone.outerHTML;
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,7 @@
1
+ document.addEventListener('turbo:before-fetch-request', () => {
2
+ document.getElementById('main-progress-bar').classList.remove('invisible');
3
+ });
4
+
5
+ document.addEventListener('turbo:before-fetch-response', () => {
6
+ document.getElementById('main-progress-bar').classList.add('invisible');
7
+ });
@@ -0,0 +1,21 @@
1
+ const requestHeaders = () => {
2
+ const meta1 = document.querySelector('meta[name="csrf-param"]');
3
+ const meta2 = document.querySelector('meta[name="csrf-token"]');
4
+ const res = { xhr: true };
5
+ res[meta1['content']] = meta2['content'];
6
+ res['X-CSRF-Token'] = meta2['content'];
7
+ return res;
8
+ }
9
+
10
+ export const ajaxRequest = async (path, data = {}, method = 'GET', format = 'text') => {
11
+ const reqData = {
12
+ method: method,
13
+ headers: requestHeaders()
14
+ };
15
+ if (method !== 'GET') reqData['body'] = data;
16
+
17
+ //TODO: show/hide loading bar
18
+ const response = await fetch(path, reqData);
19
+ const res = await (format == 'json' ? response.json() : response.text());
20
+ return res;
21
+ }
@@ -0,0 +1,61 @@
1
+ const reloadTurboFrames = (frames: string[]) => {
2
+ frames.forEach((frameSelector) => {
3
+ const frame = document.body.querySelector<HTMLIFrameElement>(frameSelector);
4
+ if (!frame) return;
5
+
6
+ const src = frame.src;
7
+ frame.src = null;
8
+ frame.src = src;
9
+ });
10
+ };
11
+
12
+ const closeActiveModal = () => {
13
+ const modal = document.body.querySelector<HTMLDivElement>(':scope .modal.show');
14
+ if (modal) modal.dispatchEvent(new CustomEvent('close-modal'));
15
+ };
16
+
17
+ // add request header to identify turbo requests
18
+ document.addEventListener('turbo:before-fetch-request', (e) => {
19
+ const target = e.target as HTMLElement;
20
+ e.detail.fetchOptions.headers['turbo-request'] = true;
21
+
22
+ // by default render response content as content of the turbo-frame
23
+ if (e.target.getAttribute('data-auto-update')) {
24
+ e.detail.fetchOptions.headers['turbo-update'] = e.target.id;
25
+ }
26
+
27
+ // TODO: disable form buttons
28
+ });
29
+
30
+ const parseRequestError = async (response) => {
31
+ const result = await response.clone().text();
32
+ const body = result.split('<body>')[1].split('</body>')[0];
33
+ const tpl = `
34
+ <div data-controller="gaco-cms-modal"
35
+ data-gaco-cms-modal-size-value="modal-lg text-danger"
36
+ data-gaco-cms-modal-self-modal-value="true">${body}</div>
37
+ `;
38
+ document.body.insertAdjacentHTML('beforeend', tpl);
39
+ };
40
+
41
+ document.addEventListener('turbo:before-fetch-response', (e) => {
42
+ const target = e.target as HTMLElement;
43
+ const sourceTarget = e.target as HTMLElement;
44
+
45
+ // reset turbo_frame_none src
46
+ if (target.id == 'turbo_frame_none') target.removeAttribute('src');
47
+
48
+ const response = e.detail.fetchResponse.response;
49
+ if (response.status != 500) {
50
+ // allow to auto close active modal
51
+ const closeModal = sourceTarget.getAttribute('data-turbo-request-close-modal');
52
+ if (closeModal) closeActiveModal();
53
+
54
+ // reload specific turbo-frame
55
+ const frames = sourceTarget.getAttribute('data-turbo-request-reload-frame');
56
+ if (frames) reloadTurboFrames(frames.split(','));
57
+ } else {
58
+ e.preventDefault();
59
+ parseRequestError(response);
60
+ }
61
+ });
@@ -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 default application;
@@ -0,0 +1,7 @@
1
+ import '@hotwired/turbo-rails';
2
+ import './gaco_cms/lib/progress_bar';
3
+ import './gaco_cms/lib/turbo_request';
4
+
5
+ import 'tinymce/tinymce';
6
+ import './gaco_cms/base';
7
+ import * as bootstrap from 'bootstrap';
@@ -0,0 +1,3 @@
1
+ import '@hotwired/turbo-rails';
2
+ import './gaco_cms/stimulus';
3
+ import * as bootstrap from 'bootstrap';
@@ -0,0 +1,15 @@
1
+ // Bootstrap customizations
2
+ $white: #ffffff;
3
+
4
+ $theme-colors: (
5
+ "light": #d7d5d5,
6
+ "dark": #0d1127,
7
+ "primary": #8f10ad,
8
+ "secondary": #e34159,
9
+ "info": #fe6f01,
10
+ "success": #00aa6a,
11
+ "warning": #decf00,
12
+ "danger": #ed2f00,
13
+ );
14
+
15
+ $nav-link-color: #222;
@@ -0,0 +1,22 @@
1
+ .accordion {
2
+ .accordion-button {
3
+ cursor: pointer;
4
+ }
5
+ .accordion-actions {
6
+ margin-top: -42px;
7
+ position: relative;
8
+ z-index: 2;
9
+ float: left;
10
+ }
11
+ }
12
+
13
+
14
+ .fieldset_style {
15
+ border: 1px dashed #ccc;
16
+ margin-top: 32px;
17
+ .legend_style {
18
+ position: relative;
19
+ top: -16px;
20
+ padding: 0 10px;
21
+ }
22
+ }