designer 0.1.2

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 (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