gaco_cms 0.1.0

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