gaco_cms 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +48 -0
- data/Rakefile +10 -0
- data/app/assets/builds/gaco_cms.css +18257 -0
- data/app/assets/builds/gaco_cms.js +43235 -0
- data/app/assets/builds/gaco_cms.js.map +7 -0
- data/app/assets/builds/gaco_cms_front.css +18237 -0
- data/app/assets/builds/gaco_cms_front.js +10722 -0
- data/app/assets/builds/gaco_cms_front.js.map +7 -0
- data/app/assets/javascripts/gaco_cms/base.js +24 -0
- data/app/assets/javascripts/gaco_cms/controllers/confirm_controller.ts +20 -0
- data/app/assets/javascripts/gaco_cms/controllers/deletable_row_controller.ts +46 -0
- data/app/assets/javascripts/gaco_cms/controllers/editor_controller.ts +58 -0
- data/app/assets/javascripts/gaco_cms/controllers/file_input_controller.ts +82 -0
- data/app/assets/javascripts/gaco_cms/controllers/modal_controller.ts +73 -0
- data/app/assets/javascripts/gaco_cms/controllers/remote_content_controller.ts +27 -0
- data/app/assets/javascripts/gaco_cms/controllers/repeatable_field_controller.ts +26 -0
- data/app/assets/javascripts/gaco_cms/controllers/sortable_controller.ts +27 -0
- data/app/assets/javascripts/gaco_cms/controllers/toggle_field_controller.ts +37 -0
- data/app/assets/javascripts/gaco_cms/controllers/translatable_controller.ts +86 -0
- data/app/assets/javascripts/gaco_cms/lib/progress_bar.ts +7 -0
- data/app/assets/javascripts/gaco_cms/lib/request.ts +21 -0
- data/app/assets/javascripts/gaco_cms/lib/turbo_request.ts +61 -0
- data/app/assets/javascripts/gaco_cms/stimulus.ts +9 -0
- data/app/assets/javascripts/gaco_cms.js +7 -0
- data/app/assets/javascripts/gaco_cms_front.js +3 -0
- data/app/assets/stylesheets/gaco_cms/bootstrap_custom.scss +15 -0
- data/app/assets/stylesheets/gaco_cms/shared.scss +22 -0
- data/app/assets/stylesheets/gaco_cms.css.scss +8 -0
- data/app/assets/stylesheets/gaco_cms_front.css.scss +6 -0
- data/app/controllers/concerns/gaco_cms/turbo_concern.rb +52 -0
- data/app/controllers/gaco_cms/admin/base_controller.rb +20 -0
- data/app/controllers/gaco_cms/admin/field_groups_controller.rb +87 -0
- data/app/controllers/gaco_cms/admin/field_groups_manager_controller.rb +66 -0
- data/app/controllers/gaco_cms/admin/field_groups_renderer_controller.rb +33 -0
- data/app/controllers/gaco_cms/admin/fields_controller.rb +20 -0
- data/app/controllers/gaco_cms/admin/page_types_controller.rb +56 -0
- data/app/controllers/gaco_cms/admin/pages_controller.rb +70 -0
- data/app/controllers/gaco_cms/admin/themes_controller.rb +55 -0
- data/app/controllers/gaco_cms/admin_controller.rb +47 -0
- data/app/controllers/gaco_cms/front_controller.rb +39 -0
- data/app/helpers/gaco_cms/application_helper.rb +26 -0
- data/app/jobs/gaco_cms/application_job.rb +6 -0
- data/app/mailers/gaco_cms/application_mailer.rb +8 -0
- data/app/models/concerns/gaco_cms/fields_assignable.rb +40 -0
- data/app/models/concerns/gaco_cms/union_scope.rb +14 -0
- data/app/models/gaco_cms/application_record.rb +25 -0
- data/app/models/gaco_cms/draft_record.rb +16 -0
- data/app/models/gaco_cms/field.rb +78 -0
- data/app/models/gaco_cms/field_group.rb +55 -0
- data/app/models/gaco_cms/field_value.rb +77 -0
- data/app/models/gaco_cms/media_file.rb +22 -0
- data/app/models/gaco_cms/page.rb +57 -0
- data/app/models/gaco_cms/page_type.rb +38 -0
- data/app/models/gaco_cms/theme.rb +28 -0
- data/app/services/gaco_cms/application_service.rb +33 -0
- data/app/services/gaco_cms/shortcode_parser.rb +217 -0
- data/app/services/gaco_cms/theme_content_generator.rb +55 -0
- data/app/views/gaco_cms/admin/base/index.html.haml +4 -0
- data/app/views/gaco_cms/admin/field_groups_manager/_field.html.haml +53 -0
- data/app/views/gaco_cms/admin/field_groups_manager/_group.html.haml +54 -0
- data/app/views/gaco_cms/admin/field_groups_manager/index.html.haml +14 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/_field.html.haml +31 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/_fields_frame.html.haml +16 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/_group.html.haml +24 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/default_value/_page.html.haml +3 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/fields/_editor.html.haml +3 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/fields/_file.html.haml +4 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/fields/_page.html.haml +3 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/fields/_text_area.html.haml +3 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/fields/_text_field.html.haml +3 -0
- data/app/views/gaco_cms/admin/field_groups_renderer/index.html.haml +20 -0
- data/app/views/gaco_cms/admin/page_types/form.html.haml +27 -0
- data/app/views/gaco_cms/admin/page_types/index.html.haml +27 -0
- data/app/views/gaco_cms/admin/pages/edit.html.haml +57 -0
- data/app/views/gaco_cms/admin/pages/index.html.haml +23 -0
- data/app/views/gaco_cms/admin/pages/new.html.haml +16 -0
- data/app/views/gaco_cms/admin/themes/edit.html.haml +36 -0
- data/app/views/gaco_cms/admin/themes/index.html.haml +24 -0
- data/app/views/gaco_cms/admin/themes/new.html.haml +21 -0
- data/app/views/layouts/gaco_cms/_breadcrumb.html.haml +7 -0
- data/app/views/layouts/gaco_cms/_flash_messages.html.haml +13 -0
- data/app/views/layouts/gaco_cms/admin.haml +55 -0
- data/config/routes.rb +26 -0
- data/db/migrate/20230202133738_create_cms_pages.rb +16 -0
- data/db/migrate/20230202134840_create_cms_field_groups.rb +17 -0
- data/db/migrate/20230202135314_create_cms_fields.rb +21 -0
- data/db/migrate/20230202135739_create_cms_field_values.rb +16 -0
- data/db/migrate/20230202181744_create_cms_media_files.rb +10 -0
- data/db/migrate/20230206231547_create_cms_page_types.rb +23 -0
- data/db/migrate/20230209090008_create_cms_themes.rb +19 -0
- data/lib/gaco_cms/config.rb +19 -0
- data/lib/gaco_cms/engine.rb +31 -0
- data/lib/gaco_cms/version.rb +5 -0
- data/lib/gaco_cms.rb +18 -0
- data/lib/generators/gaco_cms/install_generator.rb +16 -0
- data/lib/generators/gaco_cms/install_template/gaco_cms.rb +11 -0
- data/lib/generators/gaco_cms/install_template/themes/default/index.html.haml +2 -0
- data/lib/generators/gaco_cms/install_template/themes/default/layouts/application.html.haml +34 -0
- data/lib/generators/gaco_cms/install_template/themes/default/page.html.haml +2 -0
- data/lib/tasks/auto_annotate_models.rake +59 -0
- data/lib/tasks/gaco_cms_tasks.rake +5 -0
- 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,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
|
+
}
|