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.
- checksums.yaml +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +4 -0
- data/.travis.yml +17 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +142 -0
- data/LICENSE +21 -0
- data/README.md +102 -0
- data/Rakefile +27 -0
- data/TODO.md +6 -0
- data/app/assets/stylesheets/designer.scss +189 -0
- data/app/assets/stylesheets/designer/_texy.scss +311 -0
- data/app/controllers/designer/application_controller.rb +33 -0
- data/app/controllers/designer/editor_controller.rb +50 -0
- data/app/controllers/designer/images_controller.rb +66 -0
- data/app/helpers/designer/designer_helper.rb +95 -0
- data/app/javascript/designer/DefaultInput.vue +82 -0
- data/app/javascript/designer/components/DefaultForm.vue +65 -0
- data/app/javascript/designer/components/DefaultInput.vue +82 -0
- data/app/javascript/designer/components/MediaGallery.vue +564 -0
- data/app/javascript/designer/components/SortableInputArray.vue +74 -0
- data/app/javascript/designer/editor.js +65 -0
- data/app/javascript/designer/helpers.js +73 -0
- data/app/javascript/designer/index.js +136 -0
- data/app/javascript/packs/designer.js +2 -0
- data/app/views/designer/application/notifications.js.erb +13 -0
- data/app/views/designer/editor/show.html.slim +88 -0
- data/app/views/designer/elements/_image.html.slim +11 -0
- data/app/views/designer/elements/_quote.html.slim +7 -0
- data/app/views/designer/elements/_separator.html.slim +1 -0
- data/app/views/designer/elements/_text.html.slim +5 -0
- data/app/views/layouts/designer/application.html.slim +9 -0
- data/bin/test +6 -0
- data/bin/webpack +19 -0
- data/bin/webpack-dev-server +19 -0
- data/config/routes.rb +6 -0
- data/designer.gemspec +29 -0
- data/lib/designer.rb +10 -0
- data/lib/designer/attribute.rb +15 -0
- data/lib/designer/configuration.rb +6 -0
- data/lib/designer/engine.rb +35 -0
- data/lib/designer/version.rb +5 -0
- data/lib/tasks/designer.rake +20 -0
- data/lib/templates/dev_installer.rb +13 -0
- data/lib/templates/installer.rb +11 -0
- data/package.json +33 -0
- data/yarn.lock +288 -0
- metadata +160 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
module Designer::DesignerHelper
|
2
|
+
|
3
|
+
def designer_embed element_id
|
4
|
+
element = designer_resource.elements.find{|x| x['id'] == element_id }
|
5
|
+
return "Cannot find element `#{element_id}`" unless element
|
6
|
+
|
7
|
+
designer_render element
|
8
|
+
end
|
9
|
+
|
10
|
+
def designer_render element
|
11
|
+
element = element.symbolize_keys
|
12
|
+
template_path = designer_option(:elements_template_path)
|
13
|
+
if lookup_context.exists?(element[:template], designer_option(:elements_template_path), true)
|
14
|
+
render "#{template_path}/#{element[:template]}", designer_element_options(element)
|
15
|
+
elsif lookup_context.exists?(element[:template], 'designer/elements', true)
|
16
|
+
render "designer/elements/#{element[:template]}", designer_element_options(element)
|
17
|
+
else
|
18
|
+
raise "Missing designer template `#{element[:template]}`"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def designer_render_resource resource
|
23
|
+
return unless resource&.elements
|
24
|
+
designer_set_resource resource
|
25
|
+
resource.elements.each_with_object('') do |element, html|
|
26
|
+
next if element['hidden'] || element[:hidden]
|
27
|
+
result = designer_render element
|
28
|
+
html << result if result
|
29
|
+
end.html_safe
|
30
|
+
end
|
31
|
+
|
32
|
+
def designer_element_options element
|
33
|
+
element.symbolize_keys!
|
34
|
+
options = {}
|
35
|
+
options.merge!(element[:values]) if element[:values]
|
36
|
+
options.symbolize_keys
|
37
|
+
end
|
38
|
+
|
39
|
+
def designer_attachment image_key
|
40
|
+
ActiveStorage::Attachment.joins(:blob).where(active_storage_blobs: {key: image_key}).first
|
41
|
+
end
|
42
|
+
|
43
|
+
# Render input text embeds and ERB
|
44
|
+
def designer_content text, context={}
|
45
|
+
return if text.blank?
|
46
|
+
ApplicationController.render inline: text,
|
47
|
+
assigns: { _resource: designer_resource }.merge(context)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Render input text as markdown
|
51
|
+
def designer_markdown text, options = {}
|
52
|
+
return if text.blank?
|
53
|
+
Kramdown::Document.new(text, {
|
54
|
+
syntax_highlighter_opts: {
|
55
|
+
line_numbers: nil
|
56
|
+
}
|
57
|
+
}.merge(options)).to_html
|
58
|
+
end
|
59
|
+
|
60
|
+
def designer_preview_path
|
61
|
+
path = designer_option(:preview_path) || ':resource_name/:id'
|
62
|
+
path_parts = path.split('/')
|
63
|
+
path_parts.map! do |part|
|
64
|
+
if part[0] == ':'
|
65
|
+
param = part[1..-1]
|
66
|
+
if params[param]
|
67
|
+
params[param]
|
68
|
+
else
|
69
|
+
designer_resource.send(param)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
part
|
73
|
+
end
|
74
|
+
end
|
75
|
+
path_parts.join('/')
|
76
|
+
end
|
77
|
+
|
78
|
+
def designer_option key
|
79
|
+
Designer.configuration[designer_resource_name][key]
|
80
|
+
rescue
|
81
|
+
raise "Mismatch designer option `#{val}` for `#{designer_resource_name}`"
|
82
|
+
end
|
83
|
+
|
84
|
+
def designer_resource_name
|
85
|
+
designer_resource&.model_name&.route_key || params[:resource_name]
|
86
|
+
end
|
87
|
+
|
88
|
+
def designer_set_resource resource
|
89
|
+
@_resource = resource
|
90
|
+
end
|
91
|
+
|
92
|
+
def designer_resource
|
93
|
+
@_resource || @resource
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="w-100">
|
3
|
+
<div v-if="spec.type == 'string' && spec.multiline" class="form-group">
|
4
|
+
<a href="#" @click="openMarkdownEditor('#textarea-' + object.id)" class="float-right"><i class="fa fa-edit"></i></a>
|
5
|
+
<label :for="'textarea-' + object.id" class="control-label">{{ spec.label || name | titleize }}</label>
|
6
|
+
<textarea :id="'textarea-' + object.id" v-model="object[name]" :placeholder="spec.placeholder" @change="$emit('change')" rows="5" class="form-control"></textarea>
|
7
|
+
|
8
|
+
<!-- <div class="col-sm-3 col-form-label">
|
9
|
+
{{ spec.label || name | titleize }}
|
10
|
+
<small><a href="#" @click="openMarkdownEditor('#textarea-' + object.id)">Editor</a></small>
|
11
|
+
</div>
|
12
|
+
<div class="col-sm-9">
|
13
|
+
<textarea :id="'textarea-' + object.id" :type="spec.type" v-model="object[name]" :placeholder="spec.placeholder" @change="$emit('change')" class="form-control"></textarea>
|
14
|
+
</div> -->
|
15
|
+
</div>
|
16
|
+
<div v-else-if="spec.type == 'array'" class="form-group form-row">
|
17
|
+
<label class="col-sm-3 col-form-label">{{ spec.label || name | titleize }}</label>
|
18
|
+
<div class="col-sm-9">
|
19
|
+
<tags-input element-id="tags"
|
20
|
+
v-model="object[name]"
|
21
|
+
:existing-tags="tagArrayToObject(spec.enum)"
|
22
|
+
:typeahead="true"
|
23
|
+
:typeahead-activation-threshold="0"
|
24
|
+
:only-existing-tags="!spec.custom"></tags-input>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
<div v-else-if="spec.enum" class="form-group form-row">
|
28
|
+
<label class="col-sm-3 col-form-label">{{ spec.label || name | titleize }}</label>
|
29
|
+
<div class="col-sm-9">
|
30
|
+
<select v-model="object[name]" class="form-control">
|
31
|
+
<option value="">Please select one</option>
|
32
|
+
<option v-for="(value, index) in spec.enum">{{ value }}</option>
|
33
|
+
</select>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
<div v-else-if="spec.type == 'boolean'" class="form-check">
|
37
|
+
<input type="checkbox" value="true" :id="name" v-model="object[name]" @change="$emit('change')" class="form-check-input">
|
38
|
+
<label class="form-check-label" :for="name">{{ spec.label || name | titleize }}</label>
|
39
|
+
</div>
|
40
|
+
<div v-else class="form-group form-row">
|
41
|
+
<label class="col-sm-3 col-form-label">{{ spec.label || name | titleize }}</label>
|
42
|
+
<div class="col-sm-9">
|
43
|
+
<input :type="spec.type" v-model="object[name]" :placeholder="spec.placeholder" class="form-control">
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
</template>
|
48
|
+
|
49
|
+
<script>
|
50
|
+
import Vue from 'vue/dist/vue.esm.js'
|
51
|
+
import VoerroTagsInput from '@voerro/vue-tagsinput'
|
52
|
+
// import Helpers from '../helpers'
|
53
|
+
|
54
|
+
Vue.component('tags-input', VoerroTagsInput)
|
55
|
+
|
56
|
+
export default {
|
57
|
+
props: ['spec', 'name', 'item'],
|
58
|
+
components: {
|
59
|
+
VoerroTagsInput
|
60
|
+
},
|
61
|
+
data() {
|
62
|
+
return {
|
63
|
+
object: this.item || {}
|
64
|
+
}
|
65
|
+
},
|
66
|
+
// beforeMount() {
|
67
|
+
// // console.log('DefaultInput', this.object, this.spec.default)
|
68
|
+
// // this.object[this.name] = this.object[this.name] || this.spec.default
|
69
|
+
// },
|
70
|
+
// created() {
|
71
|
+
// console.log('DefaultInput', this, this.spec, this.name, this.item)
|
72
|
+
// },
|
73
|
+
methods: {
|
74
|
+
tagArrayToObject(tags) {
|
75
|
+
return tags.reduce((acc, cur, i) => {
|
76
|
+
acc[cur] = cur
|
77
|
+
return acc
|
78
|
+
}, {})
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
</script>
|
@@ -0,0 +1,65 @@
|
|
1
|
+
<template>
|
2
|
+
<form v-bind:id="item.id" v-on:dragstart="onDragElement">
|
3
|
+
<div class="card">
|
4
|
+
<div class="card-header d-flex align-items-center drag-handle">
|
5
|
+
<a data-toggle="collapse" href="#" v-bind:data-target="'#collapse-' + item.id" class="title px-0 flex-fill text-left">{{ item.title || spec.label }}</a>
|
6
|
+
<input type="checkbox" class="mr-3" :checked="!item.hidden" @change="item.hidden = !item.hidden">
|
7
|
+
<div class="dropdown">
|
8
|
+
<button data-toggle="dropdown" type="button" class="btn btn-sm btn-secondary dropdown-toggle"></button>
|
9
|
+
<div class="dropdown-menu">
|
10
|
+
<a href="#" class="dropdown-item" @click="copyToClipboard(getEmbedCode(item.id))">Copy embed code</a>
|
11
|
+
<a href="#" class="dropdown-item" @click="$emit('remove')">Delete</a>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
<div v-bind:id="'collapse-' + item.id" class="collapse">
|
16
|
+
<div class="card-body" v-for="(property, name) in spec.properties" :key="property.name">
|
17
|
+
<!--
|
18
|
+
<br>property: -------------------------
|
19
|
+
{{property}}
|
20
|
+
<br>item: >>>
|
21
|
+
<br>{{item}}
|
22
|
+
<br>object: >>>
|
23
|
+
<br>{{object}} -->
|
24
|
+
<SortableInputArray
|
25
|
+
v-if="property.type == 'array' && property.properties"
|
26
|
+
v-bind:item="object"
|
27
|
+
v-bind:spec="property">
|
28
|
+
</SortableInputArray>
|
29
|
+
<DefaultInput
|
30
|
+
v-else
|
31
|
+
v-bind:name="name"
|
32
|
+
v-bind:item="object"
|
33
|
+
v-bind:spec="property"></DefaultInput>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</form>
|
38
|
+
</template>
|
39
|
+
|
40
|
+
<script>
|
41
|
+
import DefaultInput from './DefaultInput.vue'
|
42
|
+
import SortableInputArray from './SortableInputArray.vue'
|
43
|
+
|
44
|
+
export default {
|
45
|
+
props: ['item', 'spec'],
|
46
|
+
components: {
|
47
|
+
DefaultInput, SortableInputArray
|
48
|
+
},
|
49
|
+
data() {
|
50
|
+
return {
|
51
|
+
object: this.item && this.item.values ? this.item.values : {}
|
52
|
+
}
|
53
|
+
},
|
54
|
+
created() {
|
55
|
+
console.log('DefaultForm', this, this.item, this.spec)
|
56
|
+
},
|
57
|
+
methods: {
|
58
|
+
onDragElement(event) {
|
59
|
+
console.log('onDragElement', event, this)
|
60
|
+
event.dataTransfer.setData('id', this.item.id)
|
61
|
+
event.dataTransfer.setData('embed', this.getEmbedCode(this.item.id))
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
</script>
|
@@ -0,0 +1,82 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="w-100">
|
3
|
+
<div v-if="spec.type == 'string' && spec.multiline" class="form-group">
|
4
|
+
<a href="#" @click="openMarkdownEditor('#textarea-' + object.id)" class="float-right"><i class="fa fa-edit"></i></a>
|
5
|
+
<label :for="'textarea-' + object.id" class="control-label">{{ spec.label || name | titleize }}</label>
|
6
|
+
<textarea :id="'textarea-' + object.id" v-model="object[name]" :placeholder="spec.placeholder" @change="$emit('change')" rows="5" class="form-control"></textarea>
|
7
|
+
|
8
|
+
<!-- <div class="col-sm-3 col-form-label">
|
9
|
+
{{ spec.label || name | titleize }}
|
10
|
+
<small><a href="#" @click="openMarkdownEditor('#textarea-' + object.id)">Editor</a></small>
|
11
|
+
</div>
|
12
|
+
<div class="col-sm-9">
|
13
|
+
<textarea :id="'textarea-' + object.id" :type="spec.type" v-model="object[name]" :placeholder="spec.placeholder" @change="$emit('change')" class="form-control"></textarea>
|
14
|
+
</div> -->
|
15
|
+
</div>
|
16
|
+
<div v-else-if="spec.type == 'array'" class="form-group form-row">
|
17
|
+
<label class="col-sm-3 col-form-label">{{ spec.label || name | titleize }}</label>
|
18
|
+
<div class="col-sm-9">
|
19
|
+
<tags-input element-id="tags"
|
20
|
+
v-model="object[name]"
|
21
|
+
:existing-tags="tagArrayToObject(spec.enum)"
|
22
|
+
:typeahead="true"
|
23
|
+
:typeahead-activation-threshold="0"
|
24
|
+
:only-existing-tags="!spec.custom"></tags-input>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
<div v-else-if="spec.enum" class="form-group form-row">
|
28
|
+
<label class="col-sm-3 col-form-label">{{ spec.label || name | titleize }}</label>
|
29
|
+
<div class="col-sm-9">
|
30
|
+
<select v-model="object[name]" class="form-control">
|
31
|
+
<option value="">Please select one</option>
|
32
|
+
<option v-for="(value, index) in spec.enum">{{ value }}</option>
|
33
|
+
</select>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
<div v-else-if="spec.type == 'boolean'" class="form-check">
|
37
|
+
<input type="checkbox" value="true" :id="name" v-model="object[name]" @change="$emit('change')" class="form-check-input">
|
38
|
+
<label class="form-check-label" :for="name">{{ spec.label || name | titleize }}</label>
|
39
|
+
</div>
|
40
|
+
<div v-else class="form-group form-row">
|
41
|
+
<label class="col-sm-3 col-form-label">{{ spec.label || name | titleize }}</label>
|
42
|
+
<div class="col-sm-9">
|
43
|
+
<input :type="spec.type" v-model="object[name]" :placeholder="spec.placeholder" class="form-control">
|
44
|
+
</div>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
</template>
|
48
|
+
|
49
|
+
<script>
|
50
|
+
import Vue from 'vue/dist/vue.esm.js'
|
51
|
+
import VoerroTagsInput from '@voerro/vue-tagsinput'
|
52
|
+
// import Helpers from '../helpers'
|
53
|
+
|
54
|
+
Vue.component('tags-input', VoerroTagsInput)
|
55
|
+
|
56
|
+
export default {
|
57
|
+
props: ['spec', 'name', 'item'],
|
58
|
+
components: {
|
59
|
+
VoerroTagsInput
|
60
|
+
},
|
61
|
+
data() {
|
62
|
+
return {
|
63
|
+
object: this.item || {}
|
64
|
+
}
|
65
|
+
},
|
66
|
+
// beforeMount() {
|
67
|
+
// // console.log('DefaultInput', this.object, this.spec.default)
|
68
|
+
// // this.object[this.name] = this.object[this.name] || this.spec.default
|
69
|
+
// },
|
70
|
+
// created() {
|
71
|
+
// console.log('DefaultInput', this, this.spec, this.name, this.item)
|
72
|
+
// },
|
73
|
+
methods: {
|
74
|
+
tagArrayToObject(tags) {
|
75
|
+
return tags.reduce((acc, cur, i) => {
|
76
|
+
acc[cur] = cur
|
77
|
+
return acc
|
78
|
+
}, {})
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
</script>
|
@@ -0,0 +1,564 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="image-uploader">
|
3
|
+
<!--
|
4
|
+
<button type="button" class="btn btn-danger float-right btn-is-option" @click.prevent="showOptions = !showOptions">
|
5
|
+
<i class="fa fa-cog" aria-hidden="true"></i>
|
6
|
+
Options
|
7
|
+
</button>
|
8
|
+
<h1 id="example-title" class="example-title">Full Example</h1>
|
9
|
+
-->
|
10
|
+
|
11
|
+
<div v-show="$refs.upload && $refs.upload.dropActive" class="drop-active">
|
12
|
+
<h3>Drop files to upload</h3>
|
13
|
+
</div>
|
14
|
+
<div class="upload">
|
15
|
+
<!-- v-show="!showOptions" -->
|
16
|
+
<div class="table-responsive">
|
17
|
+
<table class="table table-hover">
|
18
|
+
<thead>
|
19
|
+
<tr>
|
20
|
+
<!-- <th>#</th> -->
|
21
|
+
<th></th>
|
22
|
+
<th>Name</th>
|
23
|
+
<th>Size</th>
|
24
|
+
<!-- <th>Speed</th> -->
|
25
|
+
<th>Status</th>
|
26
|
+
<th></th>
|
27
|
+
</tr>
|
28
|
+
</thead>
|
29
|
+
<tbody>
|
30
|
+
<tr v-if="!files.length">
|
31
|
+
<td colspan="7">
|
32
|
+
<div class="text-center p-5">
|
33
|
+
<h4>Drop files anywhere to upload<br/>or</h4>
|
34
|
+
<label :for="name" class="btn btn-lg btn-success">Select Files</label>
|
35
|
+
</div>
|
36
|
+
</td>
|
37
|
+
</tr>
|
38
|
+
<tr v-for="(file, index) in files" :key="file.id">
|
39
|
+
<!-- <td>{{index}}</td> -->
|
40
|
+
<td>
|
41
|
+
<img v-if="file.thumbnail || file.thumbnail_url" :src="file.thumbnail || file.thumbnail_url" width="50" height="auto" />
|
42
|
+
<span v-else>No Image</span>
|
43
|
+
</td>
|
44
|
+
<td>
|
45
|
+
<div class="filename">
|
46
|
+
{{file.name}}<br>
|
47
|
+
<em>{{file.kind}}</em>
|
48
|
+
</div>
|
49
|
+
<div class="progress" v-if="file.active || file.progress && file.progress !== '0.00'">
|
50
|
+
<div :class="{'progress-bar': true, 'progress-bar-striped': true, 'bg-danger': file.error, 'progress-bar-animated': file.active}" role="progressbar" :style="{width: file.progress + '%'}">{{file.progress}}%</div>
|
51
|
+
</div>
|
52
|
+
</td>
|
53
|
+
<td>{{file.size | formatSize}}</td>
|
54
|
+
|
55
|
+
<!--
|
56
|
+
<td v-if="file.speed">{{file.speed | formatSize}}</td>
|
57
|
+
<td v-else></td>
|
58
|
+
-->
|
59
|
+
|
60
|
+
<td v-if="file.error">{{file.error}}</td>
|
61
|
+
<td v-else-if="file.persisted">persisted</td>
|
62
|
+
<td v-else-if="file.success">success</td>
|
63
|
+
<td v-else-if="file.active">active</td>
|
64
|
+
<td v-else></td>
|
65
|
+
<td align="right">
|
66
|
+
<div class="btn-group btn-group-sm">
|
67
|
+
<button class="btn btn-secondary btn-sm dropdown-toggle" data-toggle="dropdown" type="button"></button>
|
68
|
+
<div class="dropdown-menu" v-if="!file.persisted && !file.success">
|
69
|
+
<!-- <a :class="{'dropdown-item': true, disabled: file.active || file.success || file.error === 'compressing'}" href="#" @click.prevent="file.active || file.success || file.error === 'compressing' ? false : onEditFileShow(file)">Edit</a> -->
|
70
|
+
<a :class="{'dropdown-item': true, disabled: !file.active}" href="#" @click.prevent="file.active ? $refs.upload.update(file, {error: 'cancel'}) : false">Cancel</a>
|
71
|
+
|
72
|
+
<a class="dropdown-item" href="#" v-if="file.active" @click.prevent="$refs.upload.update(file, {active: false})">Abort</a>
|
73
|
+
<a class="dropdown-item" href="#" v-else-if="file.error && file.error !== 'compressing' && $refs.upload.features.html5" @click.prevent="$refs.upload.update(file, {active: true, error: '', progress: '0.00'})">Retry upload</a>
|
74
|
+
<a :class="{'dropdown-item': true, disabled: file.success || file.error === 'compressing'}" href="#" v-else @click.prevent="file.success || file.error === 'compressing' ? false : $refs.upload.update(file, {active: true})">Upload</a>
|
75
|
+
|
76
|
+
<div class="dropdown-divider"></div>
|
77
|
+
<a class="dropdown-item" href="#" @click.prevent="removeFile(index)">Remove</a>
|
78
|
+
</div>
|
79
|
+
<div class="dropdown-menu" v-else>
|
80
|
+
<a class="dropdown-item" href="#" @click.prevent="copyToClipboard(file.key)">Copy key</a>
|
81
|
+
|
82
|
+
<div class="dropdown-divider"></div>
|
83
|
+
<a class="dropdown-item" href="#" @click.prevent="removeFile(index)">Remove</a>
|
84
|
+
</div>
|
85
|
+
</div>
|
86
|
+
</td>
|
87
|
+
</tr>
|
88
|
+
</tbody>
|
89
|
+
</table>
|
90
|
+
</div>
|
91
|
+
<div class="gallery-foorer mx-2">
|
92
|
+
<div class="btn-group btn-group-sm">
|
93
|
+
<file-upload
|
94
|
+
class="btn btn-success dropdown-toggle"
|
95
|
+
:post-action="postAction"
|
96
|
+
:put-action="putAction"
|
97
|
+
:extensions="extensions"
|
98
|
+
:accept="accept"
|
99
|
+
:multiple="multiple"
|
100
|
+
:directory="directory"
|
101
|
+
:size="size || 0"
|
102
|
+
:thread="thread < 1 ? 1 : (thread > 5 ? 5 : thread)"
|
103
|
+
:headers="headers"
|
104
|
+
:data="data"
|
105
|
+
:drop="drop"
|
106
|
+
:drop-directory="dropDirectory"
|
107
|
+
:add-index="addIndex"
|
108
|
+
v-model="files"
|
109
|
+
@input-filter="inputFilter"
|
110
|
+
@input-file="inputFile"
|
111
|
+
ref="upload">
|
112
|
+
<!-- <i class="fa fa-plus"></i> -->
|
113
|
+
Select
|
114
|
+
</file-upload>
|
115
|
+
<button type="button" class="btn btn-info" v-if="!$refs.upload || !$refs.upload.active" @click.prevent="$refs.upload.active = true">
|
116
|
+
<i class="fa fa-arrow-up" aria-hidden="true"></i>
|
117
|
+
Start Upload
|
118
|
+
</button>
|
119
|
+
<button type="button" class="btn btn-danger" v-else @click.prevent="$refs.upload.active = false">
|
120
|
+
<i class="fa fa-stop" aria-hidden="true"></i>
|
121
|
+
Stop Upload
|
122
|
+
</button>
|
123
|
+
</div>
|
124
|
+
|
125
|
+
<!--
|
126
|
+
<div class="footer-status">
|
127
|
+
Drop: {{$refs.upload ? $refs.upload.drop : false}},
|
128
|
+
Active: {{$refs.upload ? $refs.upload.active : false}},
|
129
|
+
Uploaded: {{$refs.upload ? $refs.upload.uploaded : true}},
|
130
|
+
Drop active: {{$refs.upload ? $refs.upload.dropActive : false}}
|
131
|
+
</div>
|
132
|
+
-->
|
133
|
+
|
134
|
+
</div>
|
135
|
+
</div>
|
136
|
+
|
137
|
+
|
138
|
+
<!--
|
139
|
+
<div class="option" v-show="showOptions">
|
140
|
+
<div class="form-group">
|
141
|
+
<label for="accept">Accept:</label>
|
142
|
+
<input type="text" id="accept" class="form-control" v-model="accept">
|
143
|
+
<small class="form-text text-muted">Allow upload mime type</small>
|
144
|
+
</div>
|
145
|
+
<div class="form-group">
|
146
|
+
<label for="extensions">Extensions:</label>
|
147
|
+
<input type="text" id="extensions" class="form-control" v-model="extensions">
|
148
|
+
<small class="form-text text-muted">Allow upload file extension</small>
|
149
|
+
</div>
|
150
|
+
<div class="form-group">
|
151
|
+
<label>PUT Upload:</label>
|
152
|
+
<div class="form-check">
|
153
|
+
<label class="form-check-label">
|
154
|
+
<input class="form-check-input" type="radio" name="put-action" id="put-action" value="" v-model="putAction"> Off
|
155
|
+
</label>
|
156
|
+
</div>
|
157
|
+
<div class="form-check">
|
158
|
+
<label class="form-check-label">
|
159
|
+
<input class="form-check-input" type="radio" name="put-action" id="put-action" value="/upload/put" v-model="putAction"> On
|
160
|
+
</label>
|
161
|
+
</div>
|
162
|
+
<small class="form-text text-muted">After the shutdown, use the POST method to upload</small>
|
163
|
+
</div>
|
164
|
+
<div class="form-group">
|
165
|
+
<label for="thread">Thread:</label>
|
166
|
+
<input type="number" max="5" min="1" id="thread" class="form-control" v-model.number="thread">
|
167
|
+
<small class="form-text text-muted">Also upload the number of files at the same time (number of threads)</small>
|
168
|
+
</div>
|
169
|
+
<div class="form-group">
|
170
|
+
<label for="size">Max size:</label>
|
171
|
+
<input type="number" min="0" id="size" class="form-control" v-model.number="size">
|
172
|
+
</div>
|
173
|
+
<div class="form-group">
|
174
|
+
<label for="minSize">Min size:</label>
|
175
|
+
<input type="number" min="0" id="minSize" class="form-control" v-model.number="minSize">
|
176
|
+
</div>
|
177
|
+
<div class="form-group">
|
178
|
+
<label for="autoCompress">Automatically compress:</label>
|
179
|
+
<input type="number" min="0" id="autoCompress" class="form-control" v-model.number="autoCompress">
|
180
|
+
<small class="form-text text-muted" v-if="autoCompress > 0">More than {{autoCompress | formatSize}} files are automatically compressed</small>
|
181
|
+
<small class="form-text text-muted" v-else>Set up automatic compression</small>
|
182
|
+
</div>
|
183
|
+
|
184
|
+
<div class="form-group">
|
185
|
+
<div class="form-check">
|
186
|
+
<label class="form-check-label">
|
187
|
+
<input type="checkbox" id="add-index" class="form-check-input" v-model="addIndex"> Start position to add
|
188
|
+
</label>
|
189
|
+
</div>
|
190
|
+
<small class="form-text text-muted">Add a file list to start the location to add</small>
|
191
|
+
</div>
|
192
|
+
|
193
|
+
<div class="form-group">
|
194
|
+
<div class="form-check">
|
195
|
+
<label class="form-check-label">
|
196
|
+
<input type="checkbox" id="drop" class="form-check-input" v-model="drop"> Drop
|
197
|
+
</label>
|
198
|
+
</div>
|
199
|
+
<small class="form-text text-muted">Drag and drop upload</small>
|
200
|
+
</div>
|
201
|
+
<div class="form-group">
|
202
|
+
<div class="form-check">
|
203
|
+
<label class="form-check-label">
|
204
|
+
<input type="checkbox" id="drop-directory" class="form-check-input" v-model="dropDirectory"> Drop directory
|
205
|
+
</label>
|
206
|
+
</div>
|
207
|
+
<small class="form-text text-muted">Not checked, filter the dragged folder</small>
|
208
|
+
</div>
|
209
|
+
<div class="form-group">
|
210
|
+
<div class="form-check">
|
211
|
+
<label class="form-check-label">
|
212
|
+
<input type="checkbox" id="upload-auto" class="form-check-input" v-model="uploadAuto"> Auto start
|
213
|
+
</label>
|
214
|
+
</div>
|
215
|
+
<small class="form-text text-muted">Automatically activate upload</small>
|
216
|
+
</div>
|
217
|
+
<div class="form-group">
|
218
|
+
<button type="button" class="btn btn-success btn-lg btn-block" @click.prevent="showOptions = !showOptions">Confirm</button>
|
219
|
+
</div>
|
220
|
+
</div>
|
221
|
+
|
222
|
+
<div :class="{'modal-backdrop': true, 'fade': true, show: addData.show}"></div>
|
223
|
+
<div :class="{modal: true, fade: true, show: addData.show}" id="modal-add-data" tabindex="-1" role="dialog">
|
224
|
+
<div class="modal-dialog" role="document">
|
225
|
+
<div class="modal-content">
|
226
|
+
<div class="modal-header">
|
227
|
+
<h5 class="modal-title">Add data</h5>
|
228
|
+
<button type="button" class="close" @click.prevent="addData.show = false">
|
229
|
+
<span>×</span>
|
230
|
+
</button>
|
231
|
+
</div>
|
232
|
+
<form @submit.prevent="onAddData">
|
233
|
+
<div class="modal-body">
|
234
|
+
<div class="form-group">
|
235
|
+
<label for="name">Name:</label>
|
236
|
+
<input type="text" class="form-control" required id="name" placeholder="Please enter a file name" v-model="addData.name">
|
237
|
+
<small class="form-text text-muted">Such as <code>filename.txt</code></small>
|
238
|
+
</div>
|
239
|
+
<div class="form-group">
|
240
|
+
<label for="type">Type:</label>
|
241
|
+
<input type="text" class="form-control" required id="type" placeholder="Please enter the MIME type" v-model="addData.type">
|
242
|
+
<small class="form-text text-muted">Such as <code>text/plain</code></small>
|
243
|
+
</div>
|
244
|
+
<div class="form-group">
|
245
|
+
<label for="content">Content:</label>
|
246
|
+
<textarea class="form-control" required id="content" rows="3" placeholder="Please enter the file contents" v-model="addData.content"></textarea>
|
247
|
+
</div>
|
248
|
+
</div>
|
249
|
+
<div class="modal-footer">
|
250
|
+
<button type="button" class="btn btn-secondary" @click.prevent="addData.show = false">Close</button>
|
251
|
+
<button type="submit" class="btn btn-success">Save</button>
|
252
|
+
</div>
|
253
|
+
</form>
|
254
|
+
</div>
|
255
|
+
</div>
|
256
|
+
</div>
|
257
|
+
|
258
|
+
|
259
|
+
<div :class="{'modal-backdrop': true, 'fade': true, show: editFile.show}"></div>
|
260
|
+
<div :class="{modal: true, fade: true, show: editFile.show}" id="modal-edit-file" tabindex="-1" role="dialog">
|
261
|
+
<div class="modal-dialog modal-lg" role="document">
|
262
|
+
<div class="modal-content">
|
263
|
+
<div class="modal-header">
|
264
|
+
<h5 class="modal-title">Edit file</h5>
|
265
|
+
<button type="button" class="close" @click.prevent="editFile.show = false">
|
266
|
+
<span>×</span>
|
267
|
+
</button>
|
268
|
+
</div>
|
269
|
+
<form @submit.prevent="onEditorFile">
|
270
|
+
<div class="modal-body">
|
271
|
+
<div class="form-group">
|
272
|
+
<label for="name">Name:</label>
|
273
|
+
<input type="text" class="form-control" required id="name" placeholder="Please enter a file name" v-model="editFile.name">
|
274
|
+
</div>
|
275
|
+
<div class="form-group" v-if="editFile.show && editFile.blob && editFile.type && editFile.type.substr(0, 6) === 'image/'">
|
276
|
+
<label>Image: </label>
|
277
|
+
<div class="edit-image">
|
278
|
+
<img :src="editFile.blob" ref="editImage" />
|
279
|
+
</div>
|
280
|
+
|
281
|
+
<div class="edit-image-tool">
|
282
|
+
<div class="btn-group" role="group">
|
283
|
+
<button type="button" class="btn btn-success" @click="editFile.cropper.rotate(-90)" title="cropper.rotate(-90)"><i class="fa fa-undo" aria-hidden="true"></i></button>
|
284
|
+
<button type="button" class="btn btn-success" @click="editFile.cropper.rotate(90)" title="cropper.rotate(90)"><i class="fa fa-repeat" aria-hidden="true"></i></button>
|
285
|
+
</div>
|
286
|
+
<div class="btn-group" role="group">
|
287
|
+
<button type="button" class="btn btn-success" @click="editFile.cropper.crop()" title="cropper.crop()"><i class="fa fa-check" aria-hidden="true"></i></button>
|
288
|
+
<button type="button" class="btn btn-success" @click="editFile.cropper.clear()" title="cropper.clear()"><i class="fa fa-remove" aria-hidden="true"></i></button>
|
289
|
+
</div>
|
290
|
+
</div>
|
291
|
+
</div>
|
292
|
+
</div>
|
293
|
+
<div class="modal-footer">
|
294
|
+
<button type="button" class="btn btn-secondary" @click.prevent="editFile.show = false">Close</button>
|
295
|
+
<button type="submit" class="btn btn-success">Save</button>
|
296
|
+
</div>
|
297
|
+
</form>
|
298
|
+
</div>
|
299
|
+
</div>
|
300
|
+
</div>
|
301
|
+
-->
|
302
|
+
</div>
|
303
|
+
</template>
|
304
|
+
|
305
|
+
<script>
|
306
|
+
// import Cropper from 'cropperjs'
|
307
|
+
// import ImageCompressor from '@xkeshi/image-compressor'
|
308
|
+
import FileUpload from 'vue-upload-component'
|
309
|
+
export default {
|
310
|
+
name: 'MediaGallery',
|
311
|
+
components: {
|
312
|
+
FileUpload,
|
313
|
+
},
|
314
|
+
props: ['uploads_path'],
|
315
|
+
data() {
|
316
|
+
return {
|
317
|
+
files: [],
|
318
|
+
accept: 'image/png,image/gif,image/jpeg,image/webp',
|
319
|
+
extensions: 'gif,jpg,jpeg,png,webp',
|
320
|
+
// extensions: ['gif', 'jpg', 'jpeg','png', 'webp'],
|
321
|
+
// extensions: /\.(gif|jpe?g|png|webp)$/i,
|
322
|
+
minSize: 1024,
|
323
|
+
size: 1024 * 1024 * 10,
|
324
|
+
multiple: true,
|
325
|
+
directory: false,
|
326
|
+
drop: true,
|
327
|
+
dropDirectory: true,
|
328
|
+
addIndex: false,
|
329
|
+
thread: 3,
|
330
|
+
name: 'file',
|
331
|
+
postAction: this.uploads_path, //'/upload/post',
|
332
|
+
putAction: null, //this.uploads_path, //'/upload/put',
|
333
|
+
headers: {
|
334
|
+
'X-Csrf-Token': 'xxxx',
|
335
|
+
},
|
336
|
+
data: {
|
337
|
+
'_csrf_token': 'xxxxxx',
|
338
|
+
},
|
339
|
+
// autoCompress: 1024 * 1024,
|
340
|
+
// uploadAuto: false,
|
341
|
+
// showOptions: false,
|
342
|
+
// addData: {
|
343
|
+
// show: false,
|
344
|
+
// name: '',
|
345
|
+
// type: '',
|
346
|
+
// content: '',
|
347
|
+
// },
|
348
|
+
// editFile: {
|
349
|
+
// show: false,
|
350
|
+
// name: '',
|
351
|
+
// }
|
352
|
+
}
|
353
|
+
},
|
354
|
+
created() {
|
355
|
+
let self = this
|
356
|
+
$.ajax({
|
357
|
+
url: this.uploads_path,
|
358
|
+
dataType: 'json'
|
359
|
+
}).done(function (result) {
|
360
|
+
console.log('files', result)
|
361
|
+
result.forEach(function(file) {
|
362
|
+
file.persisted = true
|
363
|
+
self.files.push(file)
|
364
|
+
})
|
365
|
+
})
|
366
|
+
},
|
367
|
+
// watch: {
|
368
|
+
// 'editFile.show'(newValue, oldValue) {
|
369
|
+
// if (!newValue && oldValue) {
|
370
|
+
// this.$refs.upload.update(this.editFile.id, { error: this.editFile.error || '' })
|
371
|
+
// }
|
372
|
+
// if (newValue) {
|
373
|
+
// this.$nextTick(function () {
|
374
|
+
// if (!this.$refs.editImage) {
|
375
|
+
// return
|
376
|
+
// }
|
377
|
+
// let cropper = new Cropper(this.$refs.editImage, {
|
378
|
+
// autoCrop: false,
|
379
|
+
// })
|
380
|
+
// this.editFile = {
|
381
|
+
// ...this.editFile,
|
382
|
+
// cropper
|
383
|
+
// }
|
384
|
+
// })
|
385
|
+
// }
|
386
|
+
// },
|
387
|
+
// 'addData.show'(show) {
|
388
|
+
// if (show) {
|
389
|
+
// this.addData.name = ''
|
390
|
+
// this.addData.type = ''
|
391
|
+
// this.addData.content = ''
|
392
|
+
// }
|
393
|
+
// },
|
394
|
+
// },
|
395
|
+
methods: {
|
396
|
+
removeFile(index) {
|
397
|
+
const file = this.files.splice(index, 1)[0]
|
398
|
+
console.log('removing', file)
|
399
|
+
|
400
|
+
if (file.delete_url) {
|
401
|
+
$.ajax({
|
402
|
+
type: 'DELETE',
|
403
|
+
url: file.delete_url,
|
404
|
+
// url: this.uploads_path,
|
405
|
+
data: {
|
406
|
+
key: file.key
|
407
|
+
}
|
408
|
+
})
|
409
|
+
}
|
410
|
+
this.$refs.upload.remove(file)
|
411
|
+
},
|
412
|
+
inputFilter(newFile, oldFile, prevent) {
|
413
|
+
console.log('input filter', newFile, oldFile)
|
414
|
+
if (newFile && !oldFile) {
|
415
|
+
// Before adding a file
|
416
|
+
// Filter system files or hide files
|
417
|
+
if (/(\/|^)(Thumbs\.db|desktop\.ini|\..+)$/.test(newFile.name)) {
|
418
|
+
return prevent()
|
419
|
+
}
|
420
|
+
// Filter php html js file
|
421
|
+
if (/\.(php5?|html?|jsx?)$/i.test(newFile.name)) {
|
422
|
+
return prevent()
|
423
|
+
}
|
424
|
+
// Automatic compression
|
425
|
+
// if (newFile.file && newFile.type.substr(0, 6) === 'image/' && this.autoCompress > 0 && this.autoCompress < newFile.size) {
|
426
|
+
// newFile.error = 'compressing'
|
427
|
+
// const imageCompressor = new ImageCompressor(null, {
|
428
|
+
// convertSize: Infinity,
|
429
|
+
// maxWidth: 512,
|
430
|
+
// maxHeight: 512,
|
431
|
+
// })
|
432
|
+
// imageCompressor.compress(newFile.file)
|
433
|
+
// .then((file) => {
|
434
|
+
// this.$refs.upload.update(newFile, { error: '', file, size: file.size, type: file.type })
|
435
|
+
// })
|
436
|
+
// .catch((err) => {
|
437
|
+
// this.$refs.upload.update(newFile, { error: err.message || 'compress' })
|
438
|
+
// })
|
439
|
+
// }
|
440
|
+
}
|
441
|
+
if (newFile && (!oldFile || newFile.file !== oldFile.file)) {
|
442
|
+
// Create a blob field
|
443
|
+
newFile.blob = ''
|
444
|
+
let URL = window.URL || window.webkitURL
|
445
|
+
if (URL && URL.createObjectURL) {
|
446
|
+
newFile.blob = URL.createObjectURL(newFile.file)
|
447
|
+
}
|
448
|
+
// Thumbnails
|
449
|
+
newFile.thumbnail = ''
|
450
|
+
if (newFile.blob && newFile.type.substr(0, 6) === 'image/') {
|
451
|
+
newFile.thumbnail = newFile.blob
|
452
|
+
}
|
453
|
+
}
|
454
|
+
},
|
455
|
+
// add, update, remove File Event
|
456
|
+
inputFile(newFile, oldFile) {
|
457
|
+
console.log('input file', newFile, oldFile)
|
458
|
+
if (newFile && oldFile) {
|
459
|
+
// update
|
460
|
+
if (newFile.active && !oldFile.active) {
|
461
|
+
// beforeSend
|
462
|
+
// min size
|
463
|
+
if (newFile.size >= 0 && this.minSize > 0 && newFile.size < this.minSize) {
|
464
|
+
this.$refs.upload.update(newFile, { error: 'size' })
|
465
|
+
}
|
466
|
+
}
|
467
|
+
if (newFile.progress !== oldFile.progress) {
|
468
|
+
// progress
|
469
|
+
}
|
470
|
+
if (newFile.error && !oldFile.error) {
|
471
|
+
// error
|
472
|
+
}
|
473
|
+
if (newFile.success && !oldFile.success) {
|
474
|
+
// success
|
475
|
+
newFile.persisted = true
|
476
|
+
newFile.key = newFile.response[newFile.response.length - 1].key
|
477
|
+
}
|
478
|
+
}
|
479
|
+
if (!newFile && oldFile) {
|
480
|
+
// remove
|
481
|
+
// if (oldFile.success && oldFile.response.id) {
|
482
|
+
// // $.ajax({
|
483
|
+
// // type: 'DELETE',
|
484
|
+
// // url: '/upload/delete?id=' + oldFile.response.id,
|
485
|
+
// // })
|
486
|
+
// }
|
487
|
+
}
|
488
|
+
// Automatically activate upload
|
489
|
+
if (Boolean(newFile) !== Boolean(oldFile) || oldFile.error !== newFile.error) {
|
490
|
+
if (this.uploadAuto && !this.$refs.upload.active) {
|
491
|
+
this.$refs.upload.active = true
|
492
|
+
}
|
493
|
+
}
|
494
|
+
},
|
495
|
+
copyToClipboard(text) {
|
496
|
+
const dummy = document.createElement('input')
|
497
|
+
document.body.appendChild(dummy)
|
498
|
+
dummy.value = text
|
499
|
+
dummy.select()
|
500
|
+
document.execCommand('copy', false)
|
501
|
+
dummy.remove()
|
502
|
+
toastr.info("Copied!")
|
503
|
+
console.log('copied', text)
|
504
|
+
},
|
505
|
+
alert(message) {
|
506
|
+
alert(message)
|
507
|
+
},
|
508
|
+
// onEditFileShow(file) {
|
509
|
+
// this.editFile = { ...file, show: true }
|
510
|
+
// this.$refs.upload.update(file, { error: 'edit' })
|
511
|
+
// },
|
512
|
+
// onEditorFile() {
|
513
|
+
// if (!this.$refs.upload.features.html5) {
|
514
|
+
// this.alert('Your browser does not support')
|
515
|
+
// this.editFile.show = false
|
516
|
+
// return
|
517
|
+
// }
|
518
|
+
// let data = {
|
519
|
+
// name: this.editFile.name,
|
520
|
+
// }
|
521
|
+
// if (this.editFile.cropper) {
|
522
|
+
// let binStr = atob(this.editFile.cropper.getCroppedCanvas().toDataURL(this.editFile.type).split(',')[1])
|
523
|
+
// let arr = new Uint8Array(binStr.length)
|
524
|
+
// for (let i = 0; i < binStr.length; i++) {
|
525
|
+
// arr[i] = binStr.charCodeAt(i)
|
526
|
+
// }
|
527
|
+
// data.file = new File([arr], data.name, { type: this.editFile.type })
|
528
|
+
// data.size = data.file.size
|
529
|
+
// }
|
530
|
+
// this.$refs.upload.update(this.editFile.id, data)
|
531
|
+
// this.editFile.error = ''
|
532
|
+
// this.editFile.show = false
|
533
|
+
// },
|
534
|
+
// onAddFolder() {
|
535
|
+
// if (!this.$refs.upload.features.directory) {
|
536
|
+
// this.alert('Your browser does not support')
|
537
|
+
// return
|
538
|
+
// }
|
539
|
+
// let input = this.$refs.upload.$el.querySelector('input')
|
540
|
+
// input.directory = true
|
541
|
+
// input.webkitdirectory = true
|
542
|
+
// this.directory = true
|
543
|
+
// input.onclick = null
|
544
|
+
// input.click()
|
545
|
+
// input.onclick = (e) => {
|
546
|
+
// this.directory = false
|
547
|
+
// input.directory = false
|
548
|
+
// input.webkitdirectory = false
|
549
|
+
// }
|
550
|
+
// },
|
551
|
+
// onAddData() {
|
552
|
+
// this.addData.show = false
|
553
|
+
// if (!this.$refs.upload.features.html5) {
|
554
|
+
// this.alert('Your browser does not support')
|
555
|
+
// return
|
556
|
+
// }
|
557
|
+
// let file = new window.File([this.addData.content], this.addData.name, {
|
558
|
+
// type: this.addData.type,
|
559
|
+
// })
|
560
|
+
// this.$refs.upload.add(file)
|
561
|
+
// }
|
562
|
+
}
|
563
|
+
}
|
564
|
+
</script>
|