designer 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +4 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +142 -0
  7. data/LICENSE +21 -0
  8. data/README.md +102 -0
  9. data/Rakefile +27 -0
  10. data/TODO.md +6 -0
  11. data/app/assets/stylesheets/designer.scss +189 -0
  12. data/app/assets/stylesheets/designer/_texy.scss +311 -0
  13. data/app/controllers/designer/application_controller.rb +33 -0
  14. data/app/controllers/designer/editor_controller.rb +50 -0
  15. data/app/controllers/designer/images_controller.rb +66 -0
  16. data/app/helpers/designer/designer_helper.rb +95 -0
  17. data/app/javascript/designer/DefaultInput.vue +82 -0
  18. data/app/javascript/designer/components/DefaultForm.vue +65 -0
  19. data/app/javascript/designer/components/DefaultInput.vue +82 -0
  20. data/app/javascript/designer/components/MediaGallery.vue +564 -0
  21. data/app/javascript/designer/components/SortableInputArray.vue +74 -0
  22. data/app/javascript/designer/editor.js +65 -0
  23. data/app/javascript/designer/helpers.js +73 -0
  24. data/app/javascript/designer/index.js +136 -0
  25. data/app/javascript/packs/designer.js +2 -0
  26. data/app/views/designer/application/notifications.js.erb +13 -0
  27. data/app/views/designer/editor/show.html.slim +88 -0
  28. data/app/views/designer/elements/_image.html.slim +11 -0
  29. data/app/views/designer/elements/_quote.html.slim +7 -0
  30. data/app/views/designer/elements/_separator.html.slim +1 -0
  31. data/app/views/designer/elements/_text.html.slim +5 -0
  32. data/app/views/layouts/designer/application.html.slim +9 -0
  33. data/bin/test +6 -0
  34. data/bin/webpack +19 -0
  35. data/bin/webpack-dev-server +19 -0
  36. data/config/routes.rb +6 -0
  37. data/designer.gemspec +29 -0
  38. data/lib/designer.rb +10 -0
  39. data/lib/designer/attribute.rb +15 -0
  40. data/lib/designer/configuration.rb +6 -0
  41. data/lib/designer/engine.rb +35 -0
  42. data/lib/designer/version.rb +5 -0
  43. data/lib/tasks/designer.rake +20 -0
  44. data/lib/templates/dev_installer.rb +13 -0
  45. data/lib/templates/installer.rb +11 -0
  46. data/package.json +33 -0
  47. data/yarn.lock +288 -0
  48. metadata +160 -0
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <Draggable v-model="object" :options="{handle:'.drag-handle-child'}" @update="onUpdate">
3
+ <div class="section-heading d-flex align-items-center">
4
+ <div class="title flex-fill">{{ spec.label || spec.name }}</div>
5
+ <a class="btn btn-sm btn-success" @click.prevent="object.push({})" href="#"><i class="fa fa-plus"></i> Add</a>
6
+ </div>
7
+ <div class="card child" v-for="(data, index) in object">
8
+ <div class="card-header drag-handle-child">
9
+ <a class="btn btn-sm float-right" @click.prevent="object.splice(index, 1)" href="#"><i class="fa fa-trash"></i></a>
10
+ <a class="title" data-toggle="collapse" v-bind:data-target="'#collapse-' + index" href="#">
11
+ <span v-if="data.title">{{ data.title }}</span>
12
+ <span v-else-if="data.label">{{ data.label }}</span>
13
+ <span v-else-if="data.id">Item {{ data.id }}</span>
14
+ <span v-else>Item {{ index }}</span>
15
+ </a>
16
+ </div>
17
+ <div v-bind:id="'collapse-' + index" class="collapse card-body" @change="onUpdate">
18
+ <!--
19
+ <br>data ###########################
20
+ {{data}}
21
+ <br>item ###########################
22
+ {{item}}
23
+ <br>object ###########################
24
+ {{object}}-->
25
+ <DefaultInput
26
+ v-for="(property, name) in spec.properties"
27
+ :key="property.name"
28
+ v-bind:name="name"
29
+ v-bind:item="data"
30
+ v-bind:spec="property"></DefaultInput>
31
+ </div>
32
+ </div>
33
+ </Draggable>
34
+ </template>
35
+
36
+ <script>
37
+ import DefaultInput from './DefaultInput.vue'
38
+ import Draggable from 'vuedraggable'
39
+
40
+ export default {
41
+ props: {
42
+ item: {
43
+ type: Object
44
+ },
45
+ scope: {
46
+ default: 'items',
47
+ type: String
48
+ },
49
+ spec: {
50
+ type: Object
51
+ }
52
+ },
53
+ components: {
54
+ DefaultInput, Draggable
55
+ },
56
+ created() {
57
+ // console.log('SortableInputArray', this, this.object, this.scope, this.spec)
58
+ },
59
+ data() {
60
+ return {
61
+ id: this.randomString(10),
62
+ object: this.item && this.item[this.scope] ? this.item[this.scope] : []
63
+ }
64
+ },
65
+ methods: {
66
+ onUpdate() {
67
+ // console.log('FormList onUpdate', this)
68
+
69
+ // FIXME: This is evil, but can't seem to to trigger the right event
70
+ this.$set(this.$parent.item.values, this.scope, this.object)
71
+ }
72
+ }
73
+ }
74
+ </script>
@@ -0,0 +1,65 @@
1
+ import SimpleMDE from 'simplemde/dist/simplemde.min'
2
+
3
+ export default class Editor {
4
+ constructor(element) {
5
+ let self = this
6
+
7
+ this.mde = new SimpleMDE({
8
+ element: element,
9
+ forceSync: true,
10
+ autoDownloadFontAwesome: false,
11
+ toolbar: ['bold', 'italic', 'strikethrough', 'heading', '|',
12
+ 'unordered-list', 'ordered-list', 'quote', 'code', 'table', 'link', '|',
13
+ 'clean-block', 'fullscreen']
14
+ })
15
+
16
+ // console.log(this.mde.gui.toolbar)
17
+ $(this.mde.gui.toolbar).find('.fa-header').attr('class', 'fa fa-heading')
18
+
19
+ this.mde.codemirror.on('drop', function(cm, event) {
20
+ const id = event.dataTransfer.getData('id')
21
+ const embed = event.dataTransfer.getData('embed')
22
+ console.log('handleDrop', cm, event, id, embed)
23
+
24
+ var coords = cm.coordsChar({ left: event.x, top: event.y })
25
+ cm.replaceRange(embed, coords)
26
+ event.preventDefault()
27
+ })
28
+ }
29
+ destroy() {
30
+ this.mde.toTextArea()
31
+ }
32
+ refresh() {
33
+ var self = this
34
+ setTimeout(function() { self.mde.codemirror.refresh() }, 0)
35
+ }
36
+ value() {
37
+ return this.mde.value()
38
+ }
39
+ toggleFullscreen() {
40
+ const elem = this.mde.options.element || document.documentElement;
41
+ if (!document.fullscreenElement && !document.mozFullScreenElement &&
42
+ !document.webkitFullscreenElement && !document.msFullscreenElement) {
43
+ if (elem.requestFullscreen) {
44
+ elem.requestFullscreen();
45
+ } else if (elem.msRequestFullscreen) {
46
+ elem.msRequestFullscreen();
47
+ } else if (elem.mozRequestFullScreen) {
48
+ elem.mozRequestFullScreen();
49
+ } else if (elem.webkitRequestFullscreen) {
50
+ elem.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
51
+ }
52
+ }
53
+ else {
54
+ if (document.exitFullscreen) {
55
+ document.exitFullscreen();
56
+ } else if (document.msExitFullscreen) {
57
+ document.msExitFullscreen();
58
+ } else if (document.mozCancelFullScreen) {
59
+ document.mozCancelFullScreen();
60
+ } else if (document.webkitExitFullscreen) {
61
+ document.webkitExitFullscreen();
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,73 @@
1
+ import Editor from './editor'
2
+
3
+ export default {
4
+ install(Vue, options) {
5
+
6
+ // Inject reusable component options
7
+ Vue.mixin({
8
+ methods: {
9
+ openMarkdownEditor(textareaSelector) {
10
+ let $textarea = $(textareaSelector)
11
+ const $modal = $('#markdown-editor-modal')
12
+
13
+ let editor = new Editor($modal.find('textarea')[0])
14
+ editor.mde.value($textarea.val())
15
+ $modal.modal('show')
16
+
17
+ function updateEditorHeight() {
18
+ $modal.find('.CodeMirror').height($modal.height())
19
+ }
20
+ updateEditorHeight()
21
+ $(window).on('resize', updateEditorHeight)
22
+
23
+ editor.mde.codemirror.on('change', function() {
24
+ $textarea.val(editor.value())
25
+ })
26
+
27
+ $modal.on('hidden.bs.modal', function(e) {
28
+ editor.destroy()
29
+ $(window).off('resize', updateEditorHeight)
30
+ })
31
+ },
32
+
33
+ copyToClipboard(text) {
34
+ const dummy = document.createElement('input')
35
+ document.body.appendChild(dummy)
36
+ dummy.value = text
37
+ dummy.select()
38
+ document.execCommand('copy', false)
39
+ dummy.remove()
40
+ toastr.info("Copied!")
41
+ console.log('copied', text)
42
+ },
43
+
44
+ getEmbedCode(itemId) {
45
+ return '<%= designer_embed("' + itemId + '") %>'
46
+ },
47
+
48
+ // Generate a random alphanumeric string
49
+ randomString(len) {
50
+ const p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
51
+ return [...Array(len)].reduce(a=>a+p[~~(Math.random()*p.length)],'')
52
+ }
53
+ }
54
+ })
55
+
56
+ Vue.filter('formatSize', (size) => {
57
+ if (size > 1024 * 1024 * 1024 * 1024) {
58
+ return (size / 1024 / 1024 / 1024 / 1024).toFixed(2) + ' TB'
59
+ } else if (size > 1024 * 1024 * 1024) {
60
+ return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB'
61
+ } else if (size > 1024 * 1024) {
62
+ return (size / 1024 / 1024).toFixed(2) + ' MB'
63
+ } else if (size > 1024) {
64
+ return (size / 1024).toFixed(2) + ' KB'
65
+ }
66
+ return size.toString() + ' B'
67
+ })
68
+
69
+ Vue.filter('titleize', (str) => {
70
+ return str.replace(/[A-Z]/g, ' $&').replace(/_/g, ' ').replace(/^./, str => str.toUpperCase())
71
+ })
72
+ }
73
+ }
@@ -0,0 +1,136 @@
1
+ import $ from 'jquery/dist/jquery'
2
+ // import Rails from 'rails-ujs/lib/assets/compiled/rails-ujs'
3
+ import 'popper.js/dist/popper'
4
+ import 'bootstrap/dist/js/bootstrap'
5
+ import 'jquery-serializejson/jquery.serializejson'
6
+ import 'jquery-resizable-dom/src/jquery-resizable'
7
+ import * as toastr from 'toastr/toastr'
8
+ import Vue from 'vue/dist/vue.esm.js'
9
+
10
+ import MediaGallery from '../designer/components/MediaGallery.vue'
11
+ import DefaultForm from '../designer/components/DefaultForm.vue'
12
+
13
+ import Draggable from 'vuedraggable'
14
+ import Helpers from '../designer/helpers'
15
+
16
+
17
+ export default {
18
+ start() {
19
+
20
+ // Set globals for erb.js callbacks
21
+ window.$ = $
22
+ window.toastr = toastr
23
+
24
+ toastr.options = {
25
+ "positionClass": "toast-bottom-right"
26
+ }
27
+
28
+ // Load the Vue app on DOMContentLoaded
29
+ if (/complete|interactive|loaded/.test(document.readyState)) {
30
+ this.load()
31
+ } else {
32
+ document.addEventListener('DOMContentLoaded', this.load, false)
33
+ }
34
+ },
35
+ load() {
36
+ // document.addEventListener('DOMContentLoaded', () => {
37
+
38
+ Vue.use(Helpers)
39
+
40
+ this.app = new Vue({
41
+ el: '#app',
42
+ components: {
43
+ MediaGallery, Draggable, DefaultForm
44
+ },
45
+ props: ['elements', 'spec', 'resource_id', 'resource_type', 'resource_path', 'preview_path', 'upload_path'],
46
+ beforeMount() {
47
+ console.log('before mount', this.$attrs, this.$el.dataset.elements, this.$el.dataset.spec)
48
+ const dataset = this.$el.dataset
49
+ for(const key in dataset) {
50
+ this[key] = (dataset[key][0] === '{' || dataset[key][0] === '[') ? JSON.parse(dataset[key]) : dataset[key]
51
+ }
52
+ this.elements = this.filterMetadata()
53
+ },
54
+ data() {
55
+ return {
56
+ showPreview: true
57
+ }
58
+ },
59
+ methods: {
60
+ getSpec(template) {
61
+ return Object.values(this.spec).find(function(item) {
62
+ return item.template == template
63
+ })
64
+ },
65
+ filterMetadata() {
66
+ var self = this
67
+ if (!Array.isArray(this.elements))
68
+ return []
69
+ return this.elements.map(function(item) {
70
+ if (!item.id)
71
+ item.id = item.template + '-' + self.randomString(10)
72
+ const found = Object.values(self.spec).find(function(definition) {
73
+ return definition.template == item.template
74
+ })
75
+ if (found) {
76
+ return item
77
+ } else {
78
+ alert('No template exists for: ' + JSON.stringify(item))
79
+ return null
80
+ }
81
+ }).filter(Boolean)
82
+ },
83
+ addElement(template) {
84
+ this.elements.push({
85
+ id: template + '-' + this.randomString(10),
86
+ template: template,
87
+ values: {}
88
+ })
89
+ },
90
+ save() {
91
+ console.log('saving', this.elements)
92
+ let self = this
93
+ let data = $('#editor form').serializeJSON()
94
+ if (!data || !data.resource)
95
+ data = { resource: {} }
96
+ data.resource.elements = JSON.stringify(this.elements)
97
+
98
+ return $.ajax({
99
+ method: 'PATCH',
100
+ url: this.resource_path,
101
+ data: data
102
+ }).done((result) => {
103
+ console.log('saved', result)
104
+ self.refreshPreview()
105
+ })
106
+ // return false
107
+ },
108
+ refreshPreview() {
109
+ document.getElementById('preview').contentWindow.location.reload()
110
+ }
111
+ }
112
+ })
113
+
114
+ //
115
+ // Resizable Panels
116
+ // TODO: Use Vue
117
+
118
+ $('#sidebar').resizable({
119
+ handleSelector: '.splitter',
120
+ resizeHeight: false
121
+ })
122
+
123
+ //
124
+ // Markdown Editor
125
+
126
+ // var $markdown = $('textarea.markdown')
127
+ // if ($markdown.length) {
128
+ // let editor = new Editor($markdown[0])
129
+ //
130
+ // $('#sidebar [rel="editor"]').click(() => {
131
+ // editor.refresh() // ensure content is redered
132
+ // })
133
+ // }
134
+ // })
135
+ }
136
+ }
@@ -0,0 +1,2 @@
1
+ import Designer from 'designer'
2
+ Designer.start()
@@ -0,0 +1,13 @@
1
+ <% if flash.any? %>
2
+ toastr.clear()
3
+ <% end %>
4
+
5
+ <% flash.each do |type, message| %>
6
+ <% if type == 'error' %>
7
+ toastr.error("<%= message %>")
8
+ <% elsif type == 'notice' %>
9
+ toastr.success("<%= message %>")
10
+ <% else %>
11
+ toastr.info("<%= message %>")
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,88 @@
1
+ - resource_id ||= @resource.id
2
+ - resource_type ||= @resource.class.name
3
+ - elements ||= @resource.elements.to_json
4
+ - spec ||= designer_option(:spec).to_json
5
+ - resource_path ||= editor_path(id: @resource.id, resource_name: resource_name)
6
+ - uploads_path ||= images_path(id: @resource.id, resource_name: resource_name)
7
+ - preview_path ||= designer_preview_path
8
+
9
+ #app {
10
+ data-resource_id=(resource_id.to_json)
11
+ data-resource_type=(resource_type)
12
+ data-elements=(elements)
13
+ data-spec=(spec)
14
+ data-resource_path=(resource_path)
15
+ data-preview_path=(preview_path)
16
+ data-uploads_path=(uploads_path)
17
+ }
18
+
19
+ #sidebar :class="{'panel-left': true, 'w-100': !showPreview}"
20
+ ul#menu.nav.nav-tabs role="tablist"
21
+ li.nav-item
22
+ a.nav-link.active data-toggle="tab" href="#elements" role="tab" Elements
23
+ li.nav-item
24
+ a.nav-link data-toggle="tab" href="#gallery" role="tab" Gallery
25
+ - if lookup_context.exists?('nav', designer_option(:designer_template_path), true)
26
+ = render "#{designer_option(:designer_template_path)}/nav", resource: @resource
27
+ li.nav-item
28
+ a.nav-link data-toggle="tab" href="#editor" role="tab" Editor
29
+ / = yield(:designer_nav)
30
+ li.nav-item.flex-fill
31
+ li.nav-item
32
+ a.nav-link href="#" @click="refreshPreview()" title="Refresh Preview"
33
+ i class="fa fa-sync"
34
+ li.nav-item
35
+ a.nav-link href="#" @click="save()" title="Save"
36
+ i class="fa fa-save"
37
+ /li.nav-item
38
+ a :class="{'nav-link': true, active: currentView == 'editor'}" href="#" @click="currentView = 'editor'" rel="editor" title="Editor"
39
+ i class="fa fa-edit"
40
+ li.nav-item
41
+ a :class="{'nav-link': true, active: showPreview}" href="#" @click="showPreview = !showPreview" rel="preview" title="Preview"
42
+ i class="fa fa-eye"
43
+ .tab-content
44
+ #elements.tab-pane.fade.show.active
45
+ .row.align-items-center
46
+ .col-6
47
+ / .tab-title.m-0.pl-1 Page Designer
48
+ .col-6.text-right
49
+ .dropdown.mr-2.my-2
50
+ button.btn.btn-sm.btn-success.dropdown-toggle data-toggle="dropdown" type="button" Add
51
+ .dropdown-menu
52
+ a.dropdown-item v-for="definition in spec" @click="addElement(definition.template)" href="#"= '{{definition.label}}'
53
+ Draggable v-model="elements" :options="{handle:'.drag-handle'}"
54
+ component {
55
+ :is="item.component || 'DefaultForm'"
56
+ v-for="(item, index) in elements" :key="item.id"
57
+ v-bind:item="item"
58
+ v-bind:spec="getSpec(item.template)"
59
+ v-on:remove="elements.splice(index, 1)" }
60
+ #gallery.tab-pane.fade aria-labelledby="gallery-tab" role="tabpanel"
61
+ component is="MediaGallery" v-bind:uploads_path="uploads_path"
62
+ - if lookup_context.exists?('form', designer_option(:designer_template_path), true)
63
+ / #editor.panel-right v-show="currentView == 'editor'"
64
+ #editor.tab-pane.fade.p-3 aria-labelledby="editor-tab" role="tabpanel"
65
+ = render "#{designer_option(:designer_template_path)}/form", resource: @resource, resource_path: resource_path
66
+ /- if lookup_context.exists?('tabs', designer_option(:designer_template_path), true)
67
+ = render "#{designer_option(:designer_template_path)}/tabs", resource: @resource
68
+ / = yield(:designer_tabs)
69
+ .splitter
70
+ /= yield(:designer_form)
71
+ / height="100%" width="100%" src=(preview_path)
72
+ iframe#preview.panel-right v-show="showPreview" height="100%" width="100%" src=(preview_path)
73
+
74
+ / Markdown Editor
75
+ #markdown-editor-modal.modal role="dialog" tabindex="-1"
76
+ .modal-dialog role="document"
77
+ .modal-content
78
+ /.modal-header
79
+ h5.modal-title Modal title
80
+ button.close aria-label="Close" data-dismiss="modal" type="button"
81
+ span aria-hidden="true" ×
82
+ .modal-body.p-0.d-flex.flex-column
83
+ button.close aria-label="Close" data-dismiss="modal" type="button"
84
+ span aria-hidden="true" ×
85
+ textarea.flex-fill
86
+ /.modal-footer
87
+ button.btn.btn-primary type="button" Save changes
88
+ button.btn.btn-secondary data-dismiss="modal" type="button" Close