mango-cms 0.0.13
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.
- package/README.md +17 -0
- package/bin/mango +4 -0
- package/frontend-starter/README.md +8 -0
- package/frontend-starter/dist/_redirects +1 -0
- package/frontend-starter/dist/assets/index.00922bd5.js +99 -0
- package/frontend-starter/dist/assets/index.1781f175.css +1 -0
- package/frontend-starter/dist/favicon.png +0 -0
- package/frontend-starter/dist/index.html +53 -0
- package/frontend-starter/dist/index.js +66 -0
- package/frontend-starter/index.html +25 -0
- package/frontend-starter/index.js +197 -0
- package/frontend-starter/package-lock.json +5454 -0
- package/frontend-starter/package.json +40 -0
- package/frontend-starter/postcss.config.js +6 -0
- package/frontend-starter/public/_redirects +1 -0
- package/frontend-starter/public/favicon.png +0 -0
- package/frontend-starter/public/index.js +66 -0
- package/frontend-starter/src/App.vue +27 -0
- package/frontend-starter/src/components/layout/login.vue +212 -0
- package/frontend-starter/src/components/layout/modal.vue +113 -0
- package/frontend-starter/src/components/layout/spinner.vue +17 -0
- package/frontend-starter/src/components/pages/404.vue +28 -0
- package/frontend-starter/src/components/pages/home.vue +74 -0
- package/frontend-starter/src/components/partials/button.vue +31 -0
- package/frontend-starter/src/helpers/Mango.vue +455 -0
- package/frontend-starter/src/helpers/breakpoints.js +34 -0
- package/frontend-starter/src/helpers/darkMode.js +38 -0
- package/frontend-starter/src/helpers/email.js +32 -0
- package/frontend-starter/src/helpers/formatPhone.js +18 -0
- package/frontend-starter/src/helpers/localDB.js +315 -0
- package/frontend-starter/src/helpers/mango.js +338 -0
- package/frontend-starter/src/helpers/model.js +9 -0
- package/frontend-starter/src/helpers/multiSelect.vue +252 -0
- package/frontend-starter/src/helpers/pills.vue +75 -0
- package/frontend-starter/src/helpers/reconnecting-websocket.js +357 -0
- package/frontend-starter/src/helpers/uploadFile.vue +157 -0
- package/frontend-starter/src/helpers/uploadFiles.vue +100 -0
- package/frontend-starter/src/helpers/uploadImages.vue +89 -0
- package/frontend-starter/src/helpers/user.js +40 -0
- package/frontend-starter/src/index.css +281 -0
- package/frontend-starter/src/main.js +145 -0
- package/frontend-starter/tailwind.config.js +46 -0
- package/frontend-starter/vite.config.js +10 -0
- package/frontend-starter/yarn.lock +3380 -0
- package/mango-cms-0.0.13.tgz +0 -0
- package/package.json +24 -0
- package/src/cli.js +93 -0
- package/src/default-config/automation/index.js +37 -0
- package/src/default-config/collections/examples.js +60 -0
- package/src/default-config/config/.collections.json +1 -0
- package/src/default-config/config/globalFields.js +15 -0
- package/src/default-config/config/settings.json +23 -0
- package/src/default-config/config/statuses.js +0 -0
- package/src/default-config/config/users.js +35 -0
- package/src/default-config/endpoints/index.js +19 -0
- package/src/default-config/fields/vimeo.js +36 -0
- package/src/default-config/hooks/test.js +5 -0
- package/src/default-config/plugins/mango-stand/index.js +206 -0
- package/src/main.js +278 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full group flex flex-col relative">
|
|
3
|
+
|
|
4
|
+
<!-- <div v-if="modelSelectedEntries" class="space-y-2 mb-2">
|
|
5
|
+
<div v-for="entry in modelSelectedEntries" :key="entry.id" class="flex items-center space-x-4 border dark:border-gray-500 p-2 md:p-4 rounded-lg relative overflow-hidden">
|
|
6
|
+
<div
|
|
7
|
+
v-if="image"
|
|
8
|
+
class="rounded-full shrink-0 w-6 h-6 md:w-8 md:h-8 bg-gray-800 dark:bg-gray-700 uppercase text-white relative flex items-center justify-center bg-cover bg-center"
|
|
9
|
+
:style="`background-image: url(${entry?.image?.url})`"
|
|
10
|
+
v-html="entry?.image?.url ? '' : entry.title[0]"
|
|
11
|
+
/>
|
|
12
|
+
<div class="">{{ entry.title }}</div>
|
|
13
|
+
<button @click="remove(entry.id)" class="flex opacity-0 hover:opacity-100 bg-gray-500/75 backdrop-blur-[1px] absolute top-0 left-0 !m-0 items-center justify-center h-full w-full">
|
|
14
|
+
<i class="far fa-trash-alt text-white text-lg" />
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
</div> -->
|
|
18
|
+
|
|
19
|
+
<Draggable v-model="selectedEntries" v-show="selectedEntries?.length" item-key="id" class="divide-y dark:divide-gray-800 overflow-scroll max-h-[25vh] w-full" :class="{'mb-2': multiple}" >
|
|
20
|
+
<template #item="{element}">
|
|
21
|
+
<div class="space-y-2">
|
|
22
|
+
<div class="flex items-center space-x-4 p-2 relative w-full hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
23
|
+
<div v-if="multiple" class="cursor-pointer"><i class="fal fa-grip-lines" /></div>
|
|
24
|
+
<div
|
|
25
|
+
v-if="image"
|
|
26
|
+
class="rounded-full shrink-0 w-6 h-6 md:w-8 md:h-8 bg-gray-800 dark:bg-gray-700 uppercase text-white relative flex items-center justify-center bg-cover bg-center"
|
|
27
|
+
:style="`background-image: url(${element?.image?.url || element?.bannerImage?.url})`"
|
|
28
|
+
v-html="element?.image?.url || element?.bannerImage?.url ? '' : element?.title?.[0]"
|
|
29
|
+
/>
|
|
30
|
+
<div class="flex-grow">{{ element?.title }}</div>
|
|
31
|
+
<button @click="remove(element?.id)" class="sm:hidden group-hover:block">
|
|
32
|
+
<i class="fal fa-trash-alt text-red-500 text-lg" />
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
</Draggable>
|
|
38
|
+
|
|
39
|
+
<button class="text-sm text-gray-500 px-3 py-2 border-2 dark:border-gray-700 rounded" @click="enableSearch" v-show="!add && (!selectedEntries?.length || multiple)">{{ placeholder || `Add ${singular}` }}</button>
|
|
40
|
+
<input
|
|
41
|
+
v-show="add && (!selectedEntries?.length || multiple)"
|
|
42
|
+
ref="input"
|
|
43
|
+
type="text" :placeholder="placeholder || `Search`"
|
|
44
|
+
v-model="search" @keydown.enter="select(targetId)"
|
|
45
|
+
@keydown.up="previous" @keydown.down="next"
|
|
46
|
+
class="w-full bg-transparent"
|
|
47
|
+
:class="inputStyle"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<div class="relative hidden group-focus-within:block">
|
|
51
|
+
<div class="absolute top-0 left-0 w-full z-50">
|
|
52
|
+
<!-- v-if="search" -->
|
|
53
|
+
<Mango
|
|
54
|
+
:algoliaSearch="search"
|
|
55
|
+
:algoliaFilters="algoliaFilters"
|
|
56
|
+
:query="{fields:['id','title','image','address','speakers']}"
|
|
57
|
+
:collection="collection" :id="null"
|
|
58
|
+
v-slot="{loading}"
|
|
59
|
+
@update:data="hits = $event"
|
|
60
|
+
:disabled="!!facet"
|
|
61
|
+
class="max-w-xs bg-white dark:bg-gray-800 z-10
|
|
62
|
+
w-full rounded-lg shadow-card "
|
|
63
|
+
>
|
|
64
|
+
<div v-if="loading || loadingFacets" class="w-full flex items-center justify-center"><Spinner :small="true" class="w-8 h-8 m-8" /></div>
|
|
65
|
+
<div v-else-if="create || hits.length" class="divide-y dark:divide-gray-500 overflow-scroll max-h-60 border-2 dark:border-gray-500 rounded-lg" ref="scrollContainer">
|
|
66
|
+
<div
|
|
67
|
+
tabindex="0"
|
|
68
|
+
v-if="create && search"
|
|
69
|
+
class="flex items-center p-4 space-x-4 w-full hover:bg-green-50 cursor-pointer"
|
|
70
|
+
@click.stop="select('new')"
|
|
71
|
+
>
|
|
72
|
+
<div class="rounded-full shrink-0 w-8 h-8 bg-gray-800 flex items-center justify-center bg-green-500">
|
|
73
|
+
<i class="fa fa-plus text-white" />
|
|
74
|
+
</div>
|
|
75
|
+
<div class="">Add New</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div
|
|
78
|
+
tabindex="0"
|
|
79
|
+
v-for="entry in hits"
|
|
80
|
+
:key="entry.id"
|
|
81
|
+
class="flex items-center p-4 space-x-4 w-full cursor-pointer"
|
|
82
|
+
@mouseenter="targetId = entry.id"
|
|
83
|
+
:class="{'bg-blue-50 dark:bg-gray-900': targetId == entry.id, 'bg-green-50 dark:bg-green-600/40': selectedIds?.includes(entry.id)}"
|
|
84
|
+
:ref="setItemRef"
|
|
85
|
+
@click.stop="select(entry.id)"
|
|
86
|
+
>
|
|
87
|
+
<div v-if="selectedIds?.includes(entry?.id)" class="rounded-full shrink-0 w-8 h-8 bg-gray-800 flex items-center justify-center bg-green-500">
|
|
88
|
+
<i class="fa fa-check text-white" />
|
|
89
|
+
</div>
|
|
90
|
+
<div
|
|
91
|
+
v-else-if="image"
|
|
92
|
+
class="rounded-full shrink-0 w-8 h-8 bg-gray-700 uppercase text-white relative flex items-center justify-center bg-cover bg-center"
|
|
93
|
+
:style="`background-image: url(${entry?.image?.url})`"
|
|
94
|
+
v-html="entry?.image?.url ? '' : entry?.title?.[0]"
|
|
95
|
+
/>
|
|
96
|
+
<div class="text-left truncate">
|
|
97
|
+
<div class="truncate">{{ entry?.title }}</div>
|
|
98
|
+
<div class="text-sm self-start text-gray-500">
|
|
99
|
+
<template v-if="collection == 'churches'">{{ entry?.address?.city }}, {{ entry?.address?.state }}</template>
|
|
100
|
+
<template v-if="collection == 'sermons'">{{ entry?.speakers?.map(s => s?.title)?.join(',') }}</template>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</Mango>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<script>
|
|
113
|
+
import { algoliaAppId, algoliaSearchKey, algoliaIndex } from '../../../mango/config/settings.json'
|
|
114
|
+
import collections from '../../../mango/config/.collections.json'
|
|
115
|
+
import algoliasearch from 'algoliasearch/lite'
|
|
116
|
+
import Draggable from 'vuedraggable'
|
|
117
|
+
|
|
118
|
+
const client = algoliasearch(algoliaAppId, algoliaSearchKey);
|
|
119
|
+
const algolia = client.initIndex(algoliaIndex);
|
|
120
|
+
|
|
121
|
+
import { useModel } from '../helpers/model'
|
|
122
|
+
import Mango from './mango'
|
|
123
|
+
import Swal from 'sweetalert2'
|
|
124
|
+
export default {
|
|
125
|
+
components: {Draggable},
|
|
126
|
+
props: {
|
|
127
|
+
multiple: {type: Boolean, default: true},
|
|
128
|
+
image: {type: Boolean, default: true},
|
|
129
|
+
create: {type: Boolean, default: false},
|
|
130
|
+
collection: {},
|
|
131
|
+
placeholder: {},
|
|
132
|
+
facet: {},
|
|
133
|
+
modelSelectedIds: {default: [], type: Array},
|
|
134
|
+
modelSelectedEntries: {default: [], type: Array},
|
|
135
|
+
algoliaFilters: {default: null},
|
|
136
|
+
inputStyle: {default: null},
|
|
137
|
+
newEntryFields: {default: {}, type: Object},
|
|
138
|
+
},
|
|
139
|
+
setup(props, { emit }) {
|
|
140
|
+
return {
|
|
141
|
+
selectedIds: useModel(props, emit, 'modelSelectedIds'),
|
|
142
|
+
selectedEntries: useModel(props, emit, 'modelSelectedEntries')
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
data() {
|
|
146
|
+
return {
|
|
147
|
+
search: null,
|
|
148
|
+
targetId: null,
|
|
149
|
+
hits: [],
|
|
150
|
+
itemRefs:[],
|
|
151
|
+
// selectedIds: [],
|
|
152
|
+
// selectedEntries: [],
|
|
153
|
+
loadingFacets: false,
|
|
154
|
+
add: false,
|
|
155
|
+
singular: collections.find(c => c.name == this.collection)?.singular
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
watch: {
|
|
159
|
+
search() {
|
|
160
|
+
console.log('search changed')
|
|
161
|
+
this.targetId = null
|
|
162
|
+
if (this.facet) this.facetSearch()
|
|
163
|
+
},
|
|
164
|
+
hits() {
|
|
165
|
+
if (this.hits?.length) this.targetId = this.hits?.[0]?.id
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
methods: {
|
|
169
|
+
enableSearch() {
|
|
170
|
+
this.add = true
|
|
171
|
+
this.$nextTick(() => this.$refs.input.focus())
|
|
172
|
+
},
|
|
173
|
+
async facetSearch() {
|
|
174
|
+
this.loadingFacets = true
|
|
175
|
+
let {facetHits} = await algolia.searchForFacetValues(this.facet, this.search)
|
|
176
|
+
this.loadingFacets = false
|
|
177
|
+
console.log('facetHits', facetHits)
|
|
178
|
+
this.hits = facetHits.map((hit, index) => ({title: hit.value, id: index}))
|
|
179
|
+
console.log('this.hits', this.hits)
|
|
180
|
+
},
|
|
181
|
+
setItemRef(el) {
|
|
182
|
+
if (el) this.itemRefs.push(el)
|
|
183
|
+
},
|
|
184
|
+
async select(id) {
|
|
185
|
+
|
|
186
|
+
// Create the new entry
|
|
187
|
+
let newEntry
|
|
188
|
+
if (id == 'new') {
|
|
189
|
+
this.creatingEntry = true
|
|
190
|
+
newEntry = await Mango[this.collection].save({ title: this.search, ...this.newEntryFields })
|
|
191
|
+
this.creatingEntry = false
|
|
192
|
+
id = newEntry.id
|
|
193
|
+
if (!id) return Swal.fire({icon: 'error', title: 'Error creating new entry'})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.$emit('selected', id)
|
|
197
|
+
|
|
198
|
+
this.selectedIds = Array.isArray(this.selectedIds) ? [...this.selectedIds, id] : [id]
|
|
199
|
+
this.$emit('selectedIds', this.selectedIds)
|
|
200
|
+
|
|
201
|
+
let selectedEntry = newEntry || this.hits.find(h => h.id == id)
|
|
202
|
+
this.selectedEntries.push(selectedEntry)
|
|
203
|
+
this.$emit('selectedEntries', this.selectedEntries)
|
|
204
|
+
|
|
205
|
+
this.search = null
|
|
206
|
+
this.targetId = null
|
|
207
|
+
this.add = false
|
|
208
|
+
|
|
209
|
+
},
|
|
210
|
+
remove(id) {
|
|
211
|
+
let idIndex = this.selectedIds.findIndex(i => i == id)
|
|
212
|
+
this.selectedIds.splice(idIndex, 1)
|
|
213
|
+
|
|
214
|
+
let selectedEntryIndex = this.selectedEntries.findIndex(h => h.id == id)
|
|
215
|
+
this.selectedEntries.splice(selectedEntryIndex, 1)
|
|
216
|
+
|
|
217
|
+
this.$emit('removed', id)
|
|
218
|
+
this.$emit('selectedIds', this.selectedIds)
|
|
219
|
+
this.$emit('selectedEntries', this.selectedEntries)
|
|
220
|
+
|
|
221
|
+
this.$forceUpdate()
|
|
222
|
+
},
|
|
223
|
+
next() {
|
|
224
|
+
if (this.targetIndex < 0 && this.hits.length) this.targetId = 0
|
|
225
|
+
if (this.targetIndex+1 == this.hits.length) return
|
|
226
|
+
this.targetId = this.hits[this.targetIndex+1].id
|
|
227
|
+
let offsetTop = this.itemRefs[this.targetIndex].offsetTop
|
|
228
|
+
console.log('offsetTop', offsetTop)
|
|
229
|
+
this.$refs.scrollContainer.scrollTop = offsetTop
|
|
230
|
+
},
|
|
231
|
+
previous() {
|
|
232
|
+
if (!this.targetIndex) return
|
|
233
|
+
this.targetId = this.hits[this.targetIndex-1].id
|
|
234
|
+
let offsetTop = this.itemRefs[this.targetIndex].offsetTop
|
|
235
|
+
console.log('offsetTop', offsetTop)
|
|
236
|
+
this.$refs.scrollContainer.scrollTop = offsetTop
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
computed: {
|
|
240
|
+
targetIndex() { return this.hits.findIndex(h => this.targetId == h.id)},
|
|
241
|
+
},
|
|
242
|
+
beforeUpdate() {
|
|
243
|
+
this.itemRefs = []
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
</script>
|
|
247
|
+
|
|
248
|
+
<style lang="postcss" scoped>
|
|
249
|
+
input {
|
|
250
|
+
@apply border rounded outline-blue-400 px-3 py-2 w-full dark:bg-transparent dark:border-gray-600 dark:placeholder-gray-500
|
|
251
|
+
}
|
|
252
|
+
</style>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<Draggable v-model="selections" v-show="selections?.length" class="flex flex-wrap">
|
|
4
|
+
<template #item="{element}">
|
|
5
|
+
<button
|
|
6
|
+
@click="remove(element)"
|
|
7
|
+
class="rounded text-white px-3 py-2 mr-2 mb-2 group hover:bg-gray-400 relative text-left"
|
|
8
|
+
:class="pillBackground"
|
|
9
|
+
>
|
|
10
|
+
<span class="group-hover:text-gray-400">{{ element }}</span>
|
|
11
|
+
<div class="group-hover:flex hidden absolute inset-0 items-center justify-center h-full w-full">
|
|
12
|
+
<i class="far fa-trash-alt" />
|
|
13
|
+
</div>
|
|
14
|
+
</button>
|
|
15
|
+
</template>
|
|
16
|
+
</Draggable>
|
|
17
|
+
<div>
|
|
18
|
+
<slot :add="add">
|
|
19
|
+
<input type="text" @change="add" v-model="value" :placeholder="placeholder" class="text-base">
|
|
20
|
+
</slot>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import { useModel } from '../helpers/model'
|
|
27
|
+
import Draggable from 'vuedraggable'
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
components: {Draggable},
|
|
31
|
+
props: {
|
|
32
|
+
modelValue: {default: []},
|
|
33
|
+
placeholder: {type: String, default: 'Add a selection'},
|
|
34
|
+
pillBackground: {type: String, default: 'bg-gray-500'},
|
|
35
|
+
delimiters: {type: Array},
|
|
36
|
+
},
|
|
37
|
+
data() {
|
|
38
|
+
return {
|
|
39
|
+
value: null
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
setup(props, { emit }) {
|
|
43
|
+
return {
|
|
44
|
+
selections: useModel(props, emit, 'modelValue'),
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
watch: {
|
|
48
|
+
selections: {
|
|
49
|
+
handler() { this.$emit('selections', this.selections) },
|
|
50
|
+
deep: true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
methods: {
|
|
54
|
+
add(event) {
|
|
55
|
+
console.log('event', event)
|
|
56
|
+
let value = [this.value || event]
|
|
57
|
+
if (this.delimiters) {
|
|
58
|
+
for (let delimiter of this.delimiters) value = value.flatMap(v => v.split(delimiter))
|
|
59
|
+
}
|
|
60
|
+
this.selections = [...new Set([...this.selections, ...value])]
|
|
61
|
+
this.value = null
|
|
62
|
+
},
|
|
63
|
+
remove(element) {
|
|
64
|
+
let index = this.selections?.findIndex(s => s == element)
|
|
65
|
+
this.selections.splice(index, 1)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<style lang="postcss" scoped>
|
|
72
|
+
input {
|
|
73
|
+
@apply border rounded outline-blue-400 px-3 py-2 w-full dark:bg-transparent dark:border-gray-600 dark:placeholder-gray-600
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// MIT License:
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2010-2012, Joe Walnes
|
|
4
|
+
//
|
|
5
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
// in the Software without restriction, including without limitation the rights
|
|
8
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
// furnished to do so, subject to the following conditions:
|
|
11
|
+
//
|
|
12
|
+
// The above copyright notice and this permission notice shall be included in
|
|
13
|
+
// all copies or substantial portions of the Software.
|
|
14
|
+
//
|
|
15
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
// THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This behaves like a WebSocket in every way, except if it fails to connect,
|
|
25
|
+
* or it gets disconnected, it will repeatedly poll until it successfully connects
|
|
26
|
+
* again.
|
|
27
|
+
*
|
|
28
|
+
* It is API compatible, so when you have:
|
|
29
|
+
* ws = new WebSocket('ws://....');
|
|
30
|
+
* you can replace with:
|
|
31
|
+
* ws = new ReconnectingWebSocket('ws://....');
|
|
32
|
+
*
|
|
33
|
+
* The event stream will typically look like:
|
|
34
|
+
* onconnecting
|
|
35
|
+
* onopen
|
|
36
|
+
* onmessage
|
|
37
|
+
* onmessage
|
|
38
|
+
* onclose // lost connection
|
|
39
|
+
* onconnecting
|
|
40
|
+
* onopen // sometime later...
|
|
41
|
+
* onmessage
|
|
42
|
+
* onmessage
|
|
43
|
+
* etc...
|
|
44
|
+
*
|
|
45
|
+
* It is API compatible with the standard WebSocket API, apart from the following members:
|
|
46
|
+
*
|
|
47
|
+
* - `bufferedAmount`
|
|
48
|
+
* - `extensions`
|
|
49
|
+
* - `binaryType`
|
|
50
|
+
*
|
|
51
|
+
* Latest version: https://github.com/joewalnes/reconnecting-websocket/
|
|
52
|
+
* - Joe Walnes
|
|
53
|
+
*
|
|
54
|
+
* Syntax
|
|
55
|
+
* ======
|
|
56
|
+
* var socket = new ReconnectingWebSocket(url, protocols, options);
|
|
57
|
+
*
|
|
58
|
+
* Parameters
|
|
59
|
+
* ==========
|
|
60
|
+
* url - The url you are connecting to.
|
|
61
|
+
* protocols - Optional string or array of protocols.
|
|
62
|
+
* options - See below
|
|
63
|
+
*
|
|
64
|
+
* Options
|
|
65
|
+
* =======
|
|
66
|
+
* Options can either be passed upon instantiation or set after instantiation:
|
|
67
|
+
*
|
|
68
|
+
* var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 });
|
|
69
|
+
*
|
|
70
|
+
* or
|
|
71
|
+
*
|
|
72
|
+
* var socket = new ReconnectingWebSocket(url);
|
|
73
|
+
* socket.debug = true;
|
|
74
|
+
* socket.reconnectInterval = 4000;
|
|
75
|
+
*
|
|
76
|
+
* debug
|
|
77
|
+
* - Whether this instance should log debug messages. Accepts true or false. Default: false.
|
|
78
|
+
*
|
|
79
|
+
* automaticOpen
|
|
80
|
+
* - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close().
|
|
81
|
+
*
|
|
82
|
+
* reconnectInterval
|
|
83
|
+
* - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000.
|
|
84
|
+
*
|
|
85
|
+
* maxReconnectInterval
|
|
86
|
+
* - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000.
|
|
87
|
+
*
|
|
88
|
+
* reconnectDecay
|
|
89
|
+
* - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5.
|
|
90
|
+
*
|
|
91
|
+
* timeoutInterval
|
|
92
|
+
* - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000.
|
|
93
|
+
*
|
|
94
|
+
*/
|
|
95
|
+
export default (this, function () {
|
|
96
|
+
|
|
97
|
+
if (!('WebSocket' in window)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ReconnectingWebSocket(url, protocols, options) {
|
|
102
|
+
|
|
103
|
+
// Default settings
|
|
104
|
+
var settings = {
|
|
105
|
+
|
|
106
|
+
/** Whether this instance should log debug messages. */
|
|
107
|
+
debug: false,
|
|
108
|
+
|
|
109
|
+
/** Whether or not the websocket should attempt to connect immediately upon instantiation. */
|
|
110
|
+
automaticOpen: true,
|
|
111
|
+
|
|
112
|
+
/** The number of milliseconds to delay before attempting to reconnect. */
|
|
113
|
+
reconnectInterval: 1000,
|
|
114
|
+
/** The maximum number of milliseconds to delay a reconnection attempt. */
|
|
115
|
+
maxReconnectInterval: 30000,
|
|
116
|
+
/** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */
|
|
117
|
+
reconnectDecay: 1.5,
|
|
118
|
+
|
|
119
|
+
/** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */
|
|
120
|
+
timeoutInterval: 2000,
|
|
121
|
+
|
|
122
|
+
/** The maximum number of reconnection attempts to make. Unlimited if null. */
|
|
123
|
+
maxReconnectAttempts: null,
|
|
124
|
+
|
|
125
|
+
/** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */
|
|
126
|
+
binaryType: 'blob'
|
|
127
|
+
}
|
|
128
|
+
if (!options) { options = {}; }
|
|
129
|
+
|
|
130
|
+
// Overwrite and define settings with options if they exist.
|
|
131
|
+
for (var key in settings) {
|
|
132
|
+
if (typeof options[key] !== 'undefined') {
|
|
133
|
+
this[key] = options[key];
|
|
134
|
+
} else {
|
|
135
|
+
this[key] = settings[key];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// These should be treated as read-only properties
|
|
140
|
+
|
|
141
|
+
/** The URL as resolved by the constructor. This is always an absolute URL. Read only. */
|
|
142
|
+
this.url = url;
|
|
143
|
+
|
|
144
|
+
/** The number of attempted reconnects since starting, or the last successful connection. Read only. */
|
|
145
|
+
this.reconnectAttempts = 0;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The current state of the connection.
|
|
149
|
+
* Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
|
|
150
|
+
* Read only.
|
|
151
|
+
*/
|
|
152
|
+
this.readyState = WebSocket.CONNECTING;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A string indicating the name of the sub-protocol the server selected; this will be one of
|
|
156
|
+
* the strings specified in the protocols parameter when creating the WebSocket object.
|
|
157
|
+
* Read only.
|
|
158
|
+
*/
|
|
159
|
+
this.protocol = null;
|
|
160
|
+
|
|
161
|
+
// Private state variables
|
|
162
|
+
|
|
163
|
+
var self = this;
|
|
164
|
+
var ws;
|
|
165
|
+
var forcedClose = false;
|
|
166
|
+
var timedOut = false;
|
|
167
|
+
var eventTarget = document.createElement('div');
|
|
168
|
+
|
|
169
|
+
// Wire up "on*" properties as event handlers
|
|
170
|
+
|
|
171
|
+
eventTarget.addEventListener('open', function (event) { self.onopen(event); });
|
|
172
|
+
eventTarget.addEventListener('close', function (event) { self.onclose(event); });
|
|
173
|
+
eventTarget.addEventListener('connecting', function (event) { self.onconnecting(event); });
|
|
174
|
+
eventTarget.addEventListener('message', function (event) { self.onmessage(event); });
|
|
175
|
+
eventTarget.addEventListener('error', function (event) { self.onerror(event); });
|
|
176
|
+
|
|
177
|
+
// Expose the API required by EventTarget
|
|
178
|
+
|
|
179
|
+
this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
|
|
180
|
+
this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
|
|
181
|
+
this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* This function generates an event that is compatible with standard
|
|
185
|
+
* compliant browsers and IE9 - IE11
|
|
186
|
+
*
|
|
187
|
+
* This will prevent the error:
|
|
188
|
+
* Object doesn't support this action
|
|
189
|
+
*
|
|
190
|
+
* http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563
|
|
191
|
+
* @param s String The name that the event should use
|
|
192
|
+
* @param args Object an optional object that the event will use
|
|
193
|
+
*/
|
|
194
|
+
function generateEvent(s, args) {
|
|
195
|
+
var evt = document.createEvent("CustomEvent");
|
|
196
|
+
evt.initCustomEvent(s, false, false, args);
|
|
197
|
+
return evt;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
this.open = function (reconnectAttempt) {
|
|
201
|
+
ws = new WebSocket(self.url, protocols || []);
|
|
202
|
+
ws.binaryType = this.binaryType;
|
|
203
|
+
|
|
204
|
+
if (reconnectAttempt) {
|
|
205
|
+
if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
eventTarget.dispatchEvent(generateEvent('connecting'));
|
|
210
|
+
this.reconnectAttempts = 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (self.debug || ReconnectingWebSocket.debugAll) {
|
|
214
|
+
console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var localWs = ws;
|
|
218
|
+
var timeout = setTimeout(function () {
|
|
219
|
+
if (self.debug || ReconnectingWebSocket.debugAll) {
|
|
220
|
+
console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
|
|
221
|
+
}
|
|
222
|
+
timedOut = true;
|
|
223
|
+
localWs.close();
|
|
224
|
+
timedOut = false;
|
|
225
|
+
}, self.timeoutInterval);
|
|
226
|
+
|
|
227
|
+
ws.onopen = function (event) {
|
|
228
|
+
clearTimeout(timeout);
|
|
229
|
+
if (self.debug || ReconnectingWebSocket.debugAll) {
|
|
230
|
+
console.debug('ReconnectingWebSocket', 'onopen', self.url);
|
|
231
|
+
}
|
|
232
|
+
self.protocol = ws.protocol;
|
|
233
|
+
self.readyState = WebSocket.OPEN;
|
|
234
|
+
self.reconnectAttempts = 0;
|
|
235
|
+
var e = generateEvent('open');
|
|
236
|
+
e.isReconnect = reconnectAttempt;
|
|
237
|
+
reconnectAttempt = false;
|
|
238
|
+
eventTarget.dispatchEvent(e);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
ws.onclose = function (event) {
|
|
242
|
+
clearTimeout(timeout);
|
|
243
|
+
ws = null;
|
|
244
|
+
if (forcedClose) {
|
|
245
|
+
self.readyState = WebSocket.CLOSED;
|
|
246
|
+
eventTarget.dispatchEvent(generateEvent('close'));
|
|
247
|
+
} else {
|
|
248
|
+
self.readyState = WebSocket.CONNECTING;
|
|
249
|
+
var e = generateEvent('connecting');
|
|
250
|
+
e.code = event.code;
|
|
251
|
+
e.reason = event.reason;
|
|
252
|
+
e.wasClean = event.wasClean;
|
|
253
|
+
eventTarget.dispatchEvent(e);
|
|
254
|
+
if (!reconnectAttempt && !timedOut) {
|
|
255
|
+
if (self.debug || ReconnectingWebSocket.debugAll) {
|
|
256
|
+
console.debug('ReconnectingWebSocket', 'onclose', self.url);
|
|
257
|
+
}
|
|
258
|
+
eventTarget.dispatchEvent(generateEvent('close'));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts);
|
|
262
|
+
setTimeout(function () {
|
|
263
|
+
self.reconnectAttempts++;
|
|
264
|
+
self.open(true);
|
|
265
|
+
}, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
ws.onmessage = function (event) {
|
|
269
|
+
if (self.debug || ReconnectingWebSocket.debugAll) {
|
|
270
|
+
console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
|
|
271
|
+
}
|
|
272
|
+
var e = generateEvent('message');
|
|
273
|
+
e.data = event.data;
|
|
274
|
+
eventTarget.dispatchEvent(e);
|
|
275
|
+
};
|
|
276
|
+
ws.onerror = function (event) {
|
|
277
|
+
if (self.debug || ReconnectingWebSocket.debugAll) {
|
|
278
|
+
console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
|
|
279
|
+
}
|
|
280
|
+
eventTarget.dispatchEvent(generateEvent('error'));
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Whether or not to create a websocket upon instantiation
|
|
285
|
+
if (this.automaticOpen == true) {
|
|
286
|
+
this.open(false);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Transmits data to the server over the WebSocket connection.
|
|
291
|
+
*
|
|
292
|
+
* @param data a text string, ArrayBuffer or Blob to send to the server.
|
|
293
|
+
*/
|
|
294
|
+
this.send = function (data) {
|
|
295
|
+
if (ws) {
|
|
296
|
+
if (self.debug || ReconnectingWebSocket.debugAll) {
|
|
297
|
+
console.debug('ReconnectingWebSocket', 'send', self.url, data);
|
|
298
|
+
}
|
|
299
|
+
return ws.send(data);
|
|
300
|
+
} else {
|
|
301
|
+
throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Closes the WebSocket connection or connection attempt, if any.
|
|
307
|
+
* If the connection is already CLOSED, this method does nothing.
|
|
308
|
+
*/
|
|
309
|
+
this.close = function (code, reason) {
|
|
310
|
+
// Default CLOSE_NORMAL code
|
|
311
|
+
if (typeof code == 'undefined') {
|
|
312
|
+
code = 1000;
|
|
313
|
+
}
|
|
314
|
+
forcedClose = true;
|
|
315
|
+
if (ws) {
|
|
316
|
+
ws.close(code, reason);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Additional public API method to refresh the connection if still open (close, re-open).
|
|
322
|
+
* For example, if the app suspects bad data / missed heart beats, it can try to refresh.
|
|
323
|
+
*/
|
|
324
|
+
this.refresh = function () {
|
|
325
|
+
if (ws) {
|
|
326
|
+
ws.close();
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* An event listener to be called when the WebSocket connection's readyState changes to OPEN;
|
|
333
|
+
* this indicates that the connection is ready to send and receive data.
|
|
334
|
+
*/
|
|
335
|
+
ReconnectingWebSocket.prototype.onopen = function (event) { };
|
|
336
|
+
/** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */
|
|
337
|
+
ReconnectingWebSocket.prototype.onclose = function (event) { };
|
|
338
|
+
/** An event listener to be called when a connection begins being attempted. */
|
|
339
|
+
ReconnectingWebSocket.prototype.onconnecting = function (event) { };
|
|
340
|
+
/** An event listener to be called when a message is received from the server. */
|
|
341
|
+
ReconnectingWebSocket.prototype.onmessage = function (event) { };
|
|
342
|
+
/** An event listener to be called when an error occurs. */
|
|
343
|
+
ReconnectingWebSocket.prototype.onerror = function (event) { };
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Whether all instances of ReconnectingWebSocket should log debug messages.
|
|
347
|
+
* Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true.
|
|
348
|
+
*/
|
|
349
|
+
ReconnectingWebSocket.debugAll = false;
|
|
350
|
+
|
|
351
|
+
ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
|
|
352
|
+
ReconnectingWebSocket.OPEN = WebSocket.OPEN;
|
|
353
|
+
ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
|
|
354
|
+
ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;
|
|
355
|
+
|
|
356
|
+
return ReconnectingWebSocket;
|
|
357
|
+
})()
|