@mixd-id/web-scaffold 0.1.2301231346 → 0.1.2301231347

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixd-id/web-scaffold",
3
3
  "private": false,
4
- "version": "0.1.2301231346",
4
+ "version": "0.1.2301231347",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -1,54 +1,28 @@
1
1
  <template>
2
- <a v-if="isExternalLink" :class="computedClass" v-bind="$attrs" :href="to" target="_blank">
2
+ <a v-if="isExternalLink" :href="href" :target="target">
3
3
  <slot />
4
4
  </a>
5
- <router-link v-else :="$props" :class="computedClass">
5
+ <router-link v-else :to="href" :target="target">
6
6
  <slot />
7
7
  </router-link>
8
8
  </template>
9
9
 
10
10
  <script>
11
- import { RouterLink } from 'vue-router'
12
11
 
13
12
  export default {
14
13
 
15
14
  props: {
16
- ...RouterLink.props,
17
- to: {
18
- type: String,
19
- required: true
20
- },
21
- variant: {
22
- type: [ String, Number ],
23
- default: "primary" // primary|secondary|outline, default:primary
24
- },
25
- size: {
26
- type: [ String ],
27
- default: '' // sm|md|lg, default: md
28
- },
29
- spacing: {
30
- type: [ String, Number ],
31
- default: '' // 0|1|2|3, default: 2
32
- },
33
- state: [ String, Number ],
15
+
16
+ href: String,
17
+
18
+ target: String
19
+
34
20
  },
35
21
 
36
22
  computed: {
37
23
 
38
24
  isExternalLink() {
39
- return typeof this.to === 'string' && (this.to.startsWith('http') || this.to.startsWith('tel:'))
40
- },
41
-
42
- computedClass(){
43
-
44
- return [
45
- this.$style.button,
46
- this.$style['variant-' + this.variant],
47
- this.$style['size-' + this.size],
48
- this.$style['spacing-' + this.spacing],
49
- parseInt(this.state) === 2 ? this.$style.loading : '',
50
- ]
51
- .join(' ')
25
+ return typeof this.href === 'string' && (this.href.startsWith('http') || this.href.startsWith('tel:'))
52
26
  }
53
27
  },
54
28
  }
@@ -57,152 +31,4 @@ export default {
57
31
  <style module>
58
32
 
59
33
 
60
- .button{
61
- @apply relative inline-flex items-center justify-center;
62
- @apply whitespace-nowrap text-ellipsis overflow-hidden;
63
- }
64
- .button>*:first-child{
65
- @apply flex items-center justify-center
66
- }
67
- .button:disabled{
68
- @apply opacity-50;
69
- }
70
-
71
- .variant-,
72
- .variant-primary{
73
- @apply bg-primary-500 text-white rounded-md border-[2px] border-primary-500;
74
- }
75
- .variant-:hover,
76
- .variant-primary:hover{
77
- @apply bg-primary-600 border-primary-600;
78
- }
79
- .variant-:active,
80
- .variant-primary:active{
81
- @apply top-[1px] left-[1px] relative;
82
- }
83
- .variant-:disabled,
84
- .variant-primary:disabled{
85
- @apply top-0 left-0 cursor-not-allowed;
86
- }
87
- .variant- *,
88
- .variant-primary *{
89
- @apply text-white fill-white;
90
- }
91
- .variant-.loading *,
92
- .variant-primary.loading *{
93
- @apply fill-transparent
94
- }
95
- .variant- .svgBg,
96
- .variant-primary .svgBg{
97
- stroke: #fff;
98
- stroke-opacity: 25%;
99
- }
100
- .variant- .svgHg,
101
- .variant-primary .svgHg{
102
- fill: #fff;
103
- fill-opacity: 75%;
104
- }
105
-
106
- .variant-outline{
107
- @apply bg-transparent text-primary-500 rounded-md border-[2px] border-primary-500;
108
- }
109
- .variant-outline:hover{
110
- @apply border-primary-300 text-primary-300;
111
- }
112
- .variant-outline:active{
113
- @apply top-[1px] left-[1px] active:relative;
114
- }
115
- .variant-outline:disabled{
116
- @apply opacity-50 top-0 left-0 cursor-not-allowed;
117
- @apply text-primary-500 border-primary-500;
118
- }
119
- .variant-outline *{
120
- @apply text-primary-500 fill-primary-500;
121
- }
122
- .variant-outline.loading *{
123
- @apply fill-transparent
124
- }
125
- .variant-outline .svgBg{
126
- stroke: var(--primary);
127
- stroke-opacity: 25%;
128
- }
129
- .variant-outline .svgHg{
130
- fill: var(--primary);
131
- fill-opacity: 75%;
132
- }
133
-
134
- .variant-secondary{
135
- @apply bg-secondary-500 text-primary rounded-md border-[2px] border-secondary-500;
136
- }
137
- .variant-secondary:hover{
138
- @apply bg-secondary-400 border-secondary-400;
139
- }
140
- .variant-secondary:active{
141
- @apply top-[1px] left-[1px] relative;
142
- }
143
- .variant-secondary:disabled{
144
- @apply bg-secondary-500 border-secondary-500 opacity-50 top-0 left-0 cursor-not-allowed;
145
- }
146
- .variant-secondary *{
147
- @apply text-white fill-white;
148
- }
149
- .variant-secondary.loading *{
150
- @apply fill-transparent
151
- }
152
- .variant-secondary .svgBg{
153
- stroke: var(--text);
154
- stroke-opacity: 25%;
155
- }
156
- .variant-secondary .svgHg{
157
- fill: #fff;
158
- fill-opacity: 75%;
159
- }
160
-
161
- .variant-minimal .svgBg{
162
- stroke: var(--base-500);
163
- stroke-opacity: 25%;
164
- }
165
- .variant-minimal .svgHg{
166
- fill: var(--text);
167
- fill-opacity: 75%;
168
- }
169
-
170
- .loadingPane{
171
- @apply absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center
172
- }
173
- .loading>*:first-child{
174
- @apply opacity-0
175
- }
176
- .loading:hover{
177
- @apply bg-primary
178
- }
179
- .loading:active{
180
- @apply top-0 left-0;
181
- }
182
-
183
- .spacing-,
184
- .spacing-2{
185
- @apply p-2
186
- }
187
- .spacing-1{
188
- @apply p-1
189
- }
190
- .spacing-3{
191
- @apply p-3
192
- }
193
-
194
- .size-sm{
195
- @apply text-sm
196
- }
197
- .size-lg{
198
- @apply text-lg
199
- }
200
- .size-xl{
201
- @apply text-xl
202
- }
203
-
204
- .spinner{
205
- @apply animate-spin h-5 w-5;
206
- }
207
-
208
34
  </style>
@@ -17,7 +17,7 @@
17
17
  <path fill-rule="evenodd" clip-rule="evenodd" d="M12 20.5C16.6944 20.5 20.5 16.6944 20.5 12C20.5 7.30558 16.6944 3.5 12 3.5C7.30558 3.5 3.5 7.30558 3.5 12C3.5 16.6944 7.30558 20.5 12 20.5ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"/>
18
18
  </svg>
19
19
 
20
- <div class="my-4 text-center">
20
+ <div class="my-4 text-center flex-1 overflow-y-auto">
21
21
  <h5 class="my-1 break-all">{{ title }}</h5>
22
22
  <p class="break-all overflow-y-auto whitespace-pre-line text-left">{{ description }}</p>
23
23
  </div>
@@ -35,10 +35,8 @@
35
35
  {{ text }}
36
36
  </strong>
37
37
  </Button>
38
- <Button variant="minimal" @click="$emit('dismiss')" class="min-w-[88px]">
39
- <label class="px-4">
40
- Cancel
41
- </label>
38
+ <Button variant="minimal" @click="$emit('dismiss')" class="min-w-[88px] px-4 hover:text-primary">
39
+ Cancel
42
40
  </Button>
43
41
  </div>
44
42
  </div>
@@ -1,8 +1,6 @@
1
1
  <template>
2
2
  <button :class="computedClass" :disabled="isDisabled">
3
- <div>
4
- <slot></slot>
5
- </div>
3
+ <slot></slot>
6
4
  <div v-if="isLoading" :class="$style.loadingPane">
7
5
  <svg :class="$style.spinner" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
8
6
  <circle :class="$style.svgBg" cx="12" cy="12" r="10" stroke-width="4"></circle>
@@ -33,10 +31,7 @@ export default{
33
31
  default: '' // 0|1|2|3, default: 2
34
32
  },
35
33
 
36
- state: {
37
- type: [ String, Number ],
38
- default: 1 // -1:disabled, 1:normal, 2:loading, default: normal
39
- }
34
+ state: [ String, Number ], // -1:disabled, 1:normal, 2:loading, default: normal
40
35
 
41
36
  },
42
37
 
@@ -49,29 +44,44 @@ export default{
49
44
  this.$style['variant-' + this.variant],
50
45
  this.$style['size-' + this.size],
51
46
  this.$style['spacing-' + this.spacing],
52
- parseInt(this.state) === 2 ? this.$style.loading : '',
47
+ parseInt(this.computedState) === 2 ? this.$style.loading : '',
53
48
  ]
54
49
  .join(' ')
55
50
  },
56
51
 
57
52
  isDisabled(){
58
- return parseInt(this.state) === -1
53
+ return parseInt(this.computedState) === -1
59
54
  },
60
55
 
61
56
  isNormal(){
62
- return parseInt(this.state) !== 2
57
+ return parseInt(this.computedState) !== 2
63
58
  },
64
59
 
65
60
  isLoading(){
66
- return parseInt(this.state) === 2
61
+ return parseInt(this.computedState) === 2
62
+ },
63
+
64
+ computedState(){
65
+ return this.state ?? this._state
67
66
  }
68
67
 
69
68
  },
70
69
 
70
+ data(){
71
+ return {
72
+ _state: null
73
+ }
74
+ },
75
+
71
76
  methods:{
72
77
 
73
78
  focus(){
74
79
  this.$el.focus()
80
+ },
81
+
82
+ setState(state){
83
+ console.log('setState', state)
84
+ this._state = state
75
85
  }
76
86
 
77
87
  }
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div :class="$style.comp">
3
+ <component :is="components"></component>
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+
9
+ import * as Vue from "vue/dist/vue.esm-bundler";
10
+
11
+ export default{
12
+
13
+ props: [ 'template' ],
14
+
15
+ data(){
16
+ return {
17
+ components: null
18
+ }
19
+ },
20
+
21
+ watch: {
22
+
23
+ template: {
24
+ immediate: true,
25
+ handler(to){
26
+ this.components = Vue.compile(to)
27
+ }
28
+ }
29
+
30
+ }
31
+
32
+
33
+
34
+ }
35
+
36
+ </script>
37
+
38
+ <style module>
39
+
40
+ .comp{
41
+
42
+ }
43
+
44
+ </style>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div :class="$style.comp">
3
3
 
4
- <div class="bg-base-300 flex flex-row border-b-[1px] border-text-200">
4
+ <div class="flex flex-row">
5
5
  <button class="p-3" type="button" @click="format('bold')">
6
6
  <svg width="14" height="14" class="fill-text-400 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M0 64C0 46.3 14.3 32 32 32H80 96 224c70.7 0 128 57.3 128 128c0 31.3-11.3 60.1-30 82.3c37.1 22.4 62 63.1 62 109.7c0 70.7-57.3 128-128 128H96 80 32c-17.7 0-32-14.3-32-32s14.3-32 32-32H48V256 96H32C14.3 96 0 81.7 0 64zM224 224c35.3 0 64-28.7 64-64s-28.7-64-64-64H112V224H224zM112 288V416H256c35.3 0 64-28.7 64-64s-28.7-64-64-64H224 112z"/></svg>
7
7
  </button>
@@ -11,26 +11,24 @@
11
11
  <button class="p-3" type="button" @click="format('underline')">
12
12
  <svg width="14" height="14" class="fill-text-400 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M16 64c0-17.7 14.3-32 32-32h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H128V224c0 53 43 96 96 96s96-43 96-96V96H304c-17.7 0-32-14.3-32-32s14.3-32 32-32h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H384V224c0 88.4-71.6 160-160 160s-160-71.6-160-160V96H48C30.3 96 16 81.7 16 64zM0 448c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32z"/></svg>
13
13
  </button>
14
- <button class="p-3" type="button" ref="alignBtn" @click="contextMenu = { type:'aligns', caller:$refs.alignBtn }">
14
+ <button class="p-3" type="button" ref="alignBtn" @click="$refs.alignContext.open($refs.alignBtn)">
15
15
  <svg width="14" height="14" class="fill-text-400 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M288 64c0 17.7-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32H256c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>
16
16
  </button>
17
- <button class="p-3" type="button" ref="headingsBtn" @click="contextMenu = { type:'headings', caller:$refs.headingsBtn }">
17
+ <button class="p-3" type="button" ref="headingsBtn" @click="$refs.headingContext.open($refs.headingsBtn)">
18
18
  <svg width="14" height="14" class="fill-text-400 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M0 64C0 46.3 14.3 32 32 32H80h48c17.7 0 32 14.3 32 32s-14.3 32-32 32H112V208H336V96H320c-17.7 0-32-14.3-32-32s14.3-32 32-32h48 48c17.7 0 32 14.3 32 32s-14.3 32-32 32H400V240 416h16c17.7 0 32 14.3 32 32s-14.3 32-32 32H368 320c-17.7 0-32-14.3-32-32s14.3-32 32-32h16V272H112V416h16c17.7 0 32 14.3 32 32s-14.3 32-32 32H80 32c-17.7 0-32-14.3-32-32s14.3-32 32-32H48V240 96H32C14.3 96 0 81.7 0 64z"/></svg>
19
19
  </button>
20
- <button class="p-3" type="button" ref="listBtn" @click="contextMenu = { type:'list', caller:$refs.listBtn }">
20
+ <button class="p-3" type="button" ref="listBtn" @click="$refs.listContext.open($refs.listBtn)">
21
21
  <svg width="14" height="14" class="fill-text-400 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M64 144c26.5 0 48-21.5 48-48s-21.5-48-48-48S16 69.5 16 96s21.5 48 48 48zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM64 464c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48zm48-208c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48z"/></svg>
22
22
  </button>
23
- <button class="p-3" type="button">
23
+ <button class="p-3" type="button" @click="createImage">
24
24
  <svg width="14" height="14" class="fill-text-400 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M152 120c-26.51 0-48 21.49-48 48s21.49 48 48 48s48-21.49 48-48S178.5 120 152 120zM447.1 32h-384C28.65 32-.0091 60.65-.0091 96v320c0 35.35 28.65 64 63.1 64h384c35.35 0 64-28.65 64-64V96C511.1 60.65 483.3 32 447.1 32zM463.1 409.3l-136.8-185.9C323.8 218.8 318.1 216 312 216c-6.113 0-11.82 2.768-15.21 7.379l-106.6 144.1l-37.09-46.1c-3.441-4.279-8.934-6.809-14.77-6.809c-5.842 0-11.33 2.529-14.78 6.809l-75.52 93.81c0-.0293 0 .0293 0 0L47.99 96c0-8.822 7.178-16 16-16h384c8.822 0 16 7.178 16 16V409.3z"/></svg>
25
25
  </button>
26
- <button class="p-3" type="button">
26
+ <button class="p-3" type="button" @click="createLink">
27
27
  <svg width="14" height="14" class="fill-text-400 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/></svg>
28
28
  </button>
29
29
  </div>
30
30
 
31
- <ContextMenu :state="contextMenu && contextMenu.type === 'headings'"
32
- :caller="contextMenu ? contextMenu.caller : null"
33
- @dismiss="contextMenu = null">
31
+ <ContextMenu ref="headingContext">
34
32
  <div class="flex flex-col">
35
33
  <button class="p-3 text-4xl text-left hover:bg-primary-200" type="button" @click="format('formatblock', 'h1')">
36
34
  Heading 1
@@ -53,9 +51,7 @@
53
51
  </div>
54
52
  </ContextMenu>
55
53
 
56
- <ContextMenu :state="contextMenu && contextMenu.type === 'aligns'"
57
- :caller="contextMenu ? contextMenu.caller : null"
58
- @dismiss="contextMenu = null">
54
+ <ContextMenu ref="alignContext">
59
55
  <div class="flex flex-col">
60
56
  <button class="p-3 text-left hover:bg-primary-200 flex flex-row items-center gap-2" type="button" @click="format('justifyleft')">
61
57
  <svg width="14" height="14" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M288 64c0 17.7-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32H256c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>
@@ -76,9 +72,7 @@
76
72
  </div>
77
73
  </ContextMenu>
78
74
 
79
- <ContextMenu :state="contextMenu && contextMenu.type === 'list'"
80
- :caller="contextMenu ? contextMenu.caller : null"
81
- @dismiss="contextMenu = null">
75
+ <ContextMenu ref="listContext">
82
76
  <div class="flex flex-col">
83
77
  <button class="p-3 text-left hover:bg-primary-200 flex flex-row items-center gap-2" type="button" @click="format('insertorderedlist')">
84
78
  <svg width="14" height="14" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M24 56c0-13.3 10.7-24 24-24H80c13.3 0 24 10.7 24 24V176h16c13.3 0 24 10.7 24 24s-10.7 24-24 24H40c-13.3 0-24-10.7-24-24s10.7-24 24-24H56V80H48C34.7 80 24 69.3 24 56zM86.7 341.2c-6.5-7.4-18.3-6.9-24 1.2L51.5 357.9c-7.7 10.8-22.7 13.3-33.5 5.6s-13.3-22.7-5.6-33.5l11.1-15.6c23.7-33.2 72.3-35.6 99.2-4.9c21.3 24.4 20.8 60.9-1.1 84.7L86.8 432H120c13.3 0 24 10.7 24 24s-10.7 24-24 24H32c-9.5 0-18.2-5.6-22-14.4s-2.1-18.9 4.3-25.9l72-78c5.3-5.8 5.4-14.6 .3-20.5zM224 64H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32zm0 160H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H224c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg>
@@ -90,26 +84,137 @@
90
84
  </button>
91
85
  </div>
92
86
  </ContextMenu>
87
+
88
+ <Modal ref="imageModal" width="360" height="420" dismissable="true">
89
+ <template v-slot:head>
90
+ <div class="relative p-5">
91
+ <h3>Add Image</h3>
92
+ <div class="absolute top-0 right-0 p-2">
93
+ <button type="button" class="p-2" @click="$refs.imageModal.close()">
94
+ <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
95
+ <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
96
+ </svg>
97
+ </button>
98
+ </div>
99
+ </div>
100
+ </template>
101
+ <template v-slot:foot>
102
+ <div class="p-5">
103
+ <Button class="w-full" :disabled="!imageCanSave" @click="onImageAdd">Add Image</Button>
104
+ </div>
105
+ </template>
106
+ <div class="flex-1 flex flex-col gap-5 p-5">
107
+ <div>
108
+ <Image class="min-h-[100px] bg-base-300 rounded-xl" ref="image" :src="newImage.file" editable="true"
109
+ @click="$refs.image.edit()" @change="onImageChanged">
110
+ <template #empty="{ instance }">
111
+ <div class="absolute right-0 top-0 bottom-0 left-0 flex items-center justify-center">
112
+ Click to Add Image...
113
+ </div>
114
+ </template>
115
+ </Image>
116
+ </div>
117
+ <div class="grid grid-cols-2 gap-5">
118
+ <div>
119
+ <label>Width</label>
120
+ <Textbox class="mt-1" v-model="newImage.width" />
121
+ </div>
122
+ <div>
123
+ <label>Height</label>
124
+ <Textbox class="mt-1" v-model="newImage.height" />
125
+ </div>
126
+ <div>
127
+ <label>Max Width</label>
128
+ <Textbox class="mt-1" v-model="newImage.maxWidth" />
129
+ </div>
130
+ <div>
131
+ <label>Max Height</label>
132
+ <Textbox class="mt-1" v-model="newImage.maxHeight" />
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </Modal>
137
+
138
+ <Modal ref="linkModal" width="360" height="360" dismissable="true">
139
+ <template v-slot:head>
140
+ <div class="relative p-5">
141
+ <h3>Add Link</h3>
142
+ <div class="absolute top-0 right-0 p-2">
143
+ <button type="button" class="p-2" @click="$refs.linkModal.close()">
144
+ <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
145
+ <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
146
+ </svg>
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </template>
151
+ <template v-slot:foot>
152
+ <div class="p-5">
153
+ <Button class="w-full" :disabled="!linkCanSave" @click="addLink">Add Link</Button>
154
+ </div>
155
+ </template>
156
+ <div class="flex-1 flex flex-col gap-5 p-5">
157
+ <div>
158
+ <label>Href</label>
159
+ <Textarea class="w-full mt-1" v-model="newLink.href" rows="2"/>
160
+ </div>
161
+ <div>
162
+ <label>Text</label>
163
+ <Textbox class="w-full mt-1" v-model="newLink.text" />
164
+ </div>
165
+ <div>
166
+ <label>Target</label>
167
+ <Dropdown class="w-full mt-1" v-model="newLink.target">
168
+ <option value="">_self</option>
169
+ <option value="_blank">_blank</option>
170
+ <option value="_parent">_parent</option>
171
+ <option value="_top">_top</option>
172
+ </Dropdown>
173
+ </div>
174
+ </div>
175
+ </Modal>
93
176
 
94
- <article ref="article" contenteditable="true" v-html="html"
177
+ <article ref="article" contenteditable="true" v-html="html" @paste="onPaste"
95
178
  @input="onInput"
96
- @blur="onBlur" />
179
+ @blur="onBlur">
180
+ </article>
97
181
 
98
182
  </div>
99
183
  </template>
100
184
 
101
185
  <script>
102
186
 
187
+ import * as Vue from "vue/dist/vue.esm-bundler";
188
+ import {restoreSelection, saveSelection} from "../utils/selection";
189
+ import {downsizeImage} from "../utils/helpers.mjs";
190
+ import axios from "axios";
191
+
103
192
  export default{
104
193
 
105
194
  props:{
106
- modelValue: String
195
+ modelValue: String,
196
+ uploadImage: String
197
+ },
198
+
199
+ computed: {
200
+
201
+ linkCanSave(){
202
+ return this.newLink.href && this.newLink.text
203
+ },
204
+
205
+ imageCanSave(){
206
+ return true
207
+ }
208
+
107
209
  },
108
210
 
109
211
  data(){
110
212
  return {
111
213
  html: '',
112
- contextMenu: null
214
+ newLink: null,
215
+ newImage: null,
216
+ components: null,
217
+ selection: null
113
218
  }
114
219
  },
115
220
 
@@ -120,7 +225,7 @@ export default{
120
225
  methods: {
121
226
 
122
227
  onInput(e){
123
- this.$emit('update:modelValue', this.$refs.article.innerHTML)
228
+ this.updateModelValue()
124
229
  },
125
230
 
126
231
  onBlur(){
@@ -140,14 +245,137 @@ export default{
140
245
 
141
246
  }
142
247
 
143
- }
248
+ },
144
249
 
145
- },
250
+ createLink(){
251
+
252
+ const { text = '' } = saveSelection(this.$refs.article)
253
+
254
+ this.newLink = {
255
+ href: 'https://',
256
+ text,
257
+ target: ''
258
+ }
259
+ this.$refs.linkModal.open()
260
+ },
261
+
262
+ addLink(){
263
+
264
+ restoreSelection()
265
+
266
+ document.execCommand("insertHTML", false,
267
+ "<a style='font-size:inherit;font-family:inherit;background-color:inherit' " +
268
+ "href=\"" + this.newLink.href + "\" " +
269
+ "target=\"" + this.newLink.target + "\">" +
270
+ this.newLink.text +
271
+ "</a>");
272
+
273
+ this.newLink = null
274
+ this.$refs.linkModal.close()
275
+ },
276
+
277
+ createImage(){
278
+
279
+ saveSelection(this.$refs.article)
280
+
281
+ this.newImage = {
282
+ width: "",
283
+ height: ""
284
+ }
285
+ this.$refs.imageModal.open()
286
+ },
287
+
288
+ onImageChanged(base64, file){
289
+ this.newImage.base64 = base64
290
+ this.newImage.file = file
291
+ },
292
+
293
+ onImageAdd(){
294
+ if(this.uploadImage){
295
+ this.saveImage()
296
+ }
297
+ else{
298
+ this.addImage()
299
+ }
300
+ },
301
+
302
+ async addImage(){
303
+
304
+ restoreSelection()
305
+
306
+ const props = {}
307
+ if(this.newImage.width)
308
+ props.width = this.newImage.width
309
+ if(this.newImage.height)
310
+ props.height = this.newImage.height
311
+
312
+ let propHtml = []
313
+ for(let key in props){
314
+ propHtml.push(`${key}='${props[key]}'`)
315
+ }
316
+
317
+ document.execCommand("insertHTML", false,
318
+ "<img src=\"" + this.newImage.base64 + "\" " + propHtml.join(' ') + "/>");
319
+
320
+ this.$refs.imageModal.close()
321
+ },
322
+
323
+ async saveImage(){
324
+
325
+ const blob = await downsizeImage(this.newImage.file, this.newImage.width ?? 600)
326
+
327
+ const data = new FormData()
328
+ data.append('image', blob)
329
+
330
+ const res = await axios.post(this.uploadImage,
331
+ data,
332
+ {
333
+ headers: {
334
+ "Content-Type": "multipart/form-data",
335
+ },
336
+ }
337
+ )
338
+
339
+ const { url } = res.data
340
+
341
+ restoreSelection()
342
+
343
+ const props = {}
344
+ if(this.newImage.width)
345
+ props.width = this.newImage.width
346
+ if(this.newImage.height)
347
+ props.height = this.newImage.height
348
+
349
+ let propHtml = []
350
+ for(let key in props){
351
+ propHtml.push(`${key}='${props[key]}'`)
352
+ }
353
+
354
+ document.execCommand("insertHTML", false,
355
+ "<img src=\"" + url + "\" " + propHtml.join(' ') + "/>");
356
+
357
+ this.$refs.imageModal.close()
358
+ },
359
+
360
+ onPaste(e){
361
+ e.preventDefault()
362
+
363
+ let text = (e.clipboardData || window.clipboardData).getData("text");
364
+ const el = document.createElement('div')
365
+ el.innerHTML = text
366
+ text = el.innerText
367
+
368
+ document.execCommand("insertText", false, text);
369
+
370
+ this.updateModelValue()
371
+ },
372
+
373
+ updateModelValue(){
146
374
 
147
- watch: {
375
+ let html = this.$refs.article.innerHTML
376
+ html = html.replaceAll("<a ", "<Ahref ").replaceAll("</a>", "</Ahref>")
148
377
 
149
- modelValue(to, from){
150
- //console.log('watch modelValue', from)
378
+ this.$emit('update:modelValue', html)
151
379
  }
152
380
 
153
381
  }
@@ -159,7 +387,6 @@ export default{
159
387
  <style module>
160
388
 
161
389
  .comp{
162
- @apply flex flex-col border-[1px] border-text-200 bg-base-50 rounded-lg overflow-hidden;
163
390
  }
164
391
 
165
392
  .comp article {
@@ -198,6 +198,8 @@ export default{
198
198
 
199
199
  props:{
200
200
 
201
+ datasource: String,
202
+
201
203
  model: String,
202
204
 
203
205
  title: String,
@@ -237,6 +239,10 @@ export default{
237
239
 
238
240
  sortableColumns(){
239
241
  return Object.values(this.columns).filter((_) => _.sortable)
242
+ },
243
+
244
+ computedDataSource(){
245
+ return this.datasource ?? this.model
240
246
  }
241
247
 
242
248
  },
@@ -272,7 +278,8 @@ export default{
272
278
  methods:{
273
279
 
274
280
  patch(){
275
- this.socketEmit(`${this.model}.patch`, {}, (res) => {
281
+
282
+ this.socketEmit(`${this.computedDataSource}.patch`, {}, (res) => {
276
283
  this.columns = res.columns
277
284
  this.presets = res.presets
278
285
  this.presetIdx = res.presetIdx
@@ -324,7 +331,7 @@ export default{
324
331
  },
325
332
 
326
333
  load(){
327
- this.socketEmit(`${this.model}.load`, {
334
+ this.socketEmit(`${this.computedDataSource}.load`, {
328
335
  preset: this.preset
329
336
  }, (res) => {
330
337
  this.items = res.items
@@ -333,7 +340,7 @@ export default{
333
340
  },
334
341
 
335
342
  loadNext(){
336
- this.socketEmit(`${this.model}.load`, {
343
+ this.socketEmit(`${this.computedDataSource}.load`, {
337
344
  preset: this.preset,
338
345
  afterItem: this.items[this.items.length - 1]
339
346
  }, (res) => {
@@ -394,8 +401,6 @@ export default{
394
401
 
395
402
  onHooks(module, event, items){
396
403
 
397
- //console.log('onHooks', module, event, items)
398
-
399
404
  if(!Array.isArray(items)) return
400
405
  items = items.filter((_) => _).map((_) => {
401
406
  _['_highlight'] = 1
@@ -426,7 +431,6 @@ export default{
426
431
  case 'destroy':
427
432
  items.forEach((item) => {
428
433
  const idx = this.items.findIndex((_) => _.id === item.id)
429
- console.log(idx, item)
430
434
  if(idx >= 0){
431
435
  this.items.splice(idx, 1)
432
436
  }
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
  <Teleport to=".bW9k">
4
4
  <Transition :name="transition" @after-leave="onAfterLeave" @after-enter="$emit('show')">
5
- <div v-if="Boolean(state)" :class="computedClass" ref="modal" :style="computedStyle">
5
+ <div v-if="computedState" :class="computedClass" ref="modal" :style="computedStyle">
6
6
  <form @submit.prevent="$emit('submit')">
7
7
  <div class="modal-head">
8
8
  <slot name="head"></slot>
@@ -83,19 +83,24 @@ export default{
83
83
  width: parseInt(this.width) + 'px',
84
84
  height: parseInt(this.height) + 'px',
85
85
  }
86
- }
86
+ },
87
+
88
+ computedState(){
89
+ return this.state ? Boolean(this.state) : this._state
90
+ }
87
91
 
88
92
  },
89
93
 
90
94
  data(){
91
95
  return {
92
- isDisabled: 0
96
+ isDisabled: 0,
97
+ _state: false
93
98
  }
94
99
  },
95
100
 
96
101
  watch:{
97
102
 
98
- state(to, from){
103
+ computedState(to, from){
99
104
 
100
105
  if(to){
101
106
  let overlay = document.querySelector('.bW9k')
@@ -119,6 +124,14 @@ export default{
119
124
 
120
125
  methods: {
121
126
 
127
+ open(){
128
+ this._state = true
129
+ },
130
+
131
+ close(){
132
+ this._state = false
133
+ },
134
+
122
135
  onAfterLeave(){
123
136
  let overlay = document.querySelector('.bW9k')
124
137
  if(overlay){
@@ -142,6 +155,7 @@ export default{
142
155
  onDismiss(e){
143
156
  if(this.dismissable && this.$refs.modal && e.target.classList.contains('bW9k')){
144
157
  this.$emit('dismiss')
158
+ this.close()
145
159
  }
146
160
  },
147
161
 
@@ -85,7 +85,7 @@ export default{
85
85
  <style module>
86
86
 
87
87
  .toast{
88
- @apply max-w-[90vw] md:max-w-[640px] bg-base-400 mt-4 mx-auto rounded-lg border-[1px] border-text-50;
88
+ @apply max-w-[90vw] md:max-w-[640px] bg-base-400 mt-4 mx-auto rounded-xl border-[1px] border-text-50;
89
89
  @apply min-h-[var(--h-cp)];
90
90
  z-index: 61;
91
91
  }
package/src/index.js CHANGED
@@ -157,8 +157,9 @@ export default{
157
157
  app.component('Checkbox', defineAsyncComponent(() => import("./components/Checkbox.vue")))
158
158
  app.component('CopyToClipboard', defineAsyncComponent(() => import("./components/CopyToClipboard.vue")))
159
159
  app.component('Countdown', defineAsyncComponent(() => import("./components/Countdown.vue")))
160
- app.component('Dropdown', defineAsyncComponent(() => import("./components/Dropdown.vue")))
161
160
  app.component('Datepicker', defineAsyncComponent(() => import("./components/Datepicker.vue")))
161
+ app.component('Dropdown', defineAsyncComponent(() => import("./components/Dropdown.vue")))
162
+ app.component('DynamicTemplate', defineAsyncComponent(() => import("./components/DynamicTemplate.vue")))
162
163
  app.component('ErrorText', defineAsyncComponent(() => import("./components/ErrorText.vue")))
163
164
  app.component('Feed', defineAsyncComponent(() => import("./components/Feed.vue")))
164
165
  app.component('HTMLEditor', defineAsyncComponent(() => import("./components/HTMLEditor.vue")))
@@ -1,6 +1,95 @@
1
1
  const md5 = require("md5");
2
2
  const fs = require("fs");
3
3
 
4
+
5
+
6
+ const ceil = (num, precision = 0) => {
7
+ var p = Math.pow(10, precision)
8
+ var n = (num * p) * (1 + Number.EPSILON)
9
+ return Math.ceil(n) / p
10
+ }
11
+
12
+ const floor = (num, precision = 0) => {
13
+ var p = Math.pow(10, precision)
14
+ var n = (num * p) * (1 + Number.EPSILON)
15
+ return Math.floor(n) / p
16
+ }
17
+
18
+ const ftWildcard = (key) => {
19
+ const term = key.replace(/[\-\+\<\>\@\(\)\~]/, ' ', 'gi')
20
+
21
+ const searchTerms = []
22
+ term.split(' ').forEach((txt) => {
23
+ if(txt.length >= 3){
24
+ searchTerms.push(`+${txt}*`)
25
+ }
26
+ })
27
+ return searchTerms.join(' ')
28
+ }
29
+
30
+ const moveFile = function(oldPath, newPath, callback) {
31
+
32
+ fs.rename(oldPath, newPath, function (err) {
33
+ if (err) {
34
+ if (err.code === 'EXDEV') {
35
+ copy();
36
+ } else {
37
+ callback(err);
38
+ }
39
+ return;
40
+ }
41
+ callback();
42
+ });
43
+
44
+ function copy() {
45
+ var readStream = fs.createReadStream(oldPath);
46
+ var writeStream = fs.createWriteStream(newPath);
47
+
48
+ readStream.on('error', callback);
49
+ writeStream.on('error', callback);
50
+
51
+ readStream.on('close', function () {
52
+ fs.unlink(oldPath, callback);
53
+ });
54
+
55
+ readStream.pipe(writeStream);
56
+ }
57
+ }
58
+
59
+ const round = (num, precision = 0) => {
60
+ var p = Math.pow(10, precision)
61
+ var n = (num * p) * (1 + Number.EPSILON)
62
+ return Math.round(n) / p
63
+ }
64
+
65
+ const sanitizeMobileNumber = (mobileNumber) => {
66
+ if(typeof mobileNumber !== 'string') return mobileNumber
67
+
68
+ if(mobileNumber.length > 0){
69
+ mobileNumber = mobileNumber.replace(/[\- ]/g, '')
70
+ if(mobileNumber.substring(0, 1) === '0') mobileNumber = '+62' + mobileNumber.substring(1)
71
+ if(mobileNumber.substring(0, 1) !== '+'){
72
+ if(mobileNumber.substring(0, 2) === '62')
73
+ mobileNumber = '+' + mobileNumber
74
+ else
75
+ mobileNumber = '+62' + mobileNumber
76
+ }
77
+ if(mobileNumber.length > 0){
78
+ mobileNumber = mobileNumber[0] + mobileNumber.slice(1).replace(/\D/g, '')
79
+ }
80
+ }
81
+
82
+ return mobileNumber
83
+ }
84
+
85
+ const saveBase64 = async(data, dir = '/storage/files/images') => {
86
+ if(data.indexOf(',') >= 0){
87
+ data = data.substring(data.indexOf(',') + 1)
88
+ }
89
+ const buffer = Buffer.from(data, 'base64');
90
+ return await saveBuffer(buffer, dir)
91
+ }
92
+
4
93
  const saveBuffer = async(buffer, dir = '/storage/files/images') => {
5
94
 
6
95
  const { fileTypeFromBuffer } = await import('file-type')
@@ -17,19 +106,72 @@ const saveBuffer = async(buffer, dir = '/storage/files/images') => {
17
106
  return filename
18
107
  }
19
108
 
20
- const ftWildcard = (key) => {
21
- const term = key.replace(/[\-\+\<\>\@\(\)\~]/, ' ', 'gi')
109
+ const saveFromUrl = async(url, dir = '/storage/files/images') => {
22
110
 
23
- const searchTerms = []
24
- term.split(' ').forEach((txt) => {
25
- if(txt.length >= 3){
26
- searchTerms.push(`+${txt}*`)
27
- }
111
+ const res = await axios({
112
+ method: 'GET',
113
+ url,
114
+ responseType: 'arraybuffer',
28
115
  })
29
- return searchTerms.join(' ')
116
+ const buffer = Buffer.from(res.data)
117
+ return await saveBuffer(buffer, dir)
118
+ }
119
+
120
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
121
+
122
+ const strSlug = (title, separator) => {
123
+ if (typeof separator == 'undefined') separator = '-';
124
+
125
+ // Convert all dashes/underscores into separator
126
+ var flip = separator === '-' ? '_' : '-';
127
+ title = title.replace(flip, separator);
128
+
129
+ // Remove all characters that are not the separator, letters, numbers, or whitespace.
130
+ title = title.toLowerCase()
131
+ .replace(new RegExp('[^a-z0-9' + separator + '\\s]', 'g'), '');
132
+
133
+ // Replace all separator characters and whitespace by a single separator
134
+ title = title.replace(new RegExp('[' + separator + '\\s]+', 'g'), separator);
135
+
136
+ return title.replace(new RegExp('^[' + separator + '\\s]+|[' + separator + '\\s]+$', 'g'),'');
30
137
  }
31
138
 
139
+ const strVars = (text, vars) => {
140
+
141
+ (text.match(/\{.*?(?=\})\}/gi) ?? []).forEach((match) => {
142
+ const key = match.substring(1, match.length - 1)
143
+ const value = vars[key] ?? ''
144
+ text = text.replace(match, value)
145
+ })
146
+
147
+ return text
148
+ }
149
+
150
+ const writeStorage = (path, content, append = false) => {
151
+
152
+ if(path.startsWith('/')) path = path.substring(1)
153
+
154
+ if(append){
155
+ fs.appendFileSync(process.env.ROOT_PATH + `/storage/${path}`, content)
156
+ }
157
+ else{
158
+ fs.writeFileSync(process.env.ROOT_PATH + `/storage/${path}`, content)
159
+ }
160
+ }
161
+
162
+
32
163
  module.exports = {
164
+ ceil,
165
+ floor,
33
166
  ftWildcard,
34
- saveBuffer
167
+ moveFile,
168
+ round,
169
+ sanitizeMobileNumber,
170
+ saveBase64,
171
+ saveBuffer,
172
+ saveFromUrl,
173
+ sleep,
174
+ strSlug,
175
+ strVars,
176
+ writeStorage
35
177
  }
@@ -1,10 +1,4 @@
1
1
 
2
- let __uid = new Date().getTime()
3
-
4
- const uid = function(prefix){
5
- return (prefix ?? '') + __uid++;
6
- }
7
-
8
2
  const csvToArray = (str, delimiter = null) => {
9
3
 
10
4
  if(!delimiter){
@@ -33,16 +27,85 @@ const csvToArray = (str, delimiter = null) => {
33
27
  return arr;
34
28
  }
35
29
 
30
+ const downsizeImage = async (url, targetWidth, imageType, quality, opt = {}) => {
31
+
32
+ let defaultImageType = 'image/jpeg'
33
+
34
+ if(url instanceof File){
35
+
36
+ defaultImageType = url.type
37
+
38
+ await (new Promise((resolve, reject) => {
39
+ var reader = new FileReader();
40
+ reader.addEventListener('load', () => {
41
+ url = reader.result
42
+ resolve()
43
+ }, false)
44
+ reader.addEventListener('error', (e) => {
45
+ reject(e)
46
+ })
47
+ reader.readAsDataURL(url);
48
+ }))
49
+ }
50
+
51
+ return new Promise((resolve, reject) => {
52
+
53
+ const image = new Image()
54
+ image.addEventListener('load', () => {
55
+
56
+ const canvasWidth = image.width > targetWidth ? targetWidth : image.width
57
+ const canvasHeight = Math.round((image.height / image.width) * canvasWidth)
58
+
59
+ const canvas = document.createElement('canvas');
60
+ canvas.width = canvasWidth;
61
+ canvas.height = canvasHeight;
62
+
63
+ if(opt.constraint){
64
+ if(canvasWidth / canvasHeight !== opt.constraint)
65
+ reject(new Error('Image constraint error'))
66
+ }
67
+
68
+ const ctx = canvas.getContext("2d");
69
+ ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight);
70
+
71
+ canvas.toBlob((blob) => {
72
+ resolve(blob)
73
+ }, imageType ?? defaultImageType, quality ?? .8)
74
+ })
75
+ image.addEventListener('error', (e) => {
76
+ reject(e)
77
+ })
78
+ image.src = url
79
+ })
80
+ }
81
+
36
82
  const parseBoolean = function(exp){
37
83
  return [ 'true', 1, true ].includes(typeof exp === 'string' ?
38
84
  exp.toLowerCase() : exp)
39
85
  }
40
86
 
87
+ const strVars = function(text, vars){
88
+
89
+ (text.match(/\{.*?(?=\})\}/gi) ?? []).forEach((match) => {
90
+ const key = match.substring(1, match.length - 1)
91
+ const value = vars[key] ?? ''
92
+ text = text.replace(match, value)
93
+ })
94
+
95
+ return text
96
+ }
97
+
98
+ let __uid = new Date().getTime()
99
+ const uid = function(prefix){
100
+ return (prefix ?? '') + __uid++;
101
+ }
102
+
41
103
  export {
104
+ downsizeImage,
42
105
  uid,
43
106
  observeInit,
44
107
  csvToArray,
45
- parseBoolean
108
+ parseBoolean,
46
109
  }
47
110
 
48
111
  function observeInit(){
@@ -0,0 +1,64 @@
1
+
2
+ let selection
3
+
4
+ const saveSelection = (container) =>{
5
+
6
+ const sel = window.getSelection()
7
+ if(sel.rangeCount < 1) return
8
+ const range = window.getSelection().getRangeAt(0)
9
+
10
+ const preSelectionRange = range.cloneRange()
11
+ preSelectionRange.selectNodeContents(container)
12
+ preSelectionRange.setEnd(range.startContainer, range.startOffset)
13
+ const start = preSelectionRange.toString().length
14
+
15
+ selection = {
16
+ start: start,
17
+ end: start + range.toString().length,
18
+ container,
19
+ text: range.toString()
20
+ }
21
+
22
+ return selection
23
+ }
24
+
25
+ const restoreSelection = () => {
26
+ if(!selection) return
27
+
28
+ const range = document.createRange();
29
+ range.setStart(selection.container, 0);
30
+ range.collapse(true);
31
+
32
+ const savedSel = selection
33
+
34
+ let charIndex = 0
35
+ let nodeStack = [selection.container], node, foundStart = false, stop = false;
36
+ while (!stop && (node = nodeStack.pop())) {
37
+ if (node.nodeType === 3) {
38
+ var nextCharIndex = charIndex + node.length;
39
+ if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
40
+ range.setStart(node, savedSel.start - charIndex);
41
+ foundStart = true;
42
+ }
43
+ if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
44
+ range.setEnd(node, savedSel.end - charIndex);
45
+ stop = true;
46
+ }
47
+ charIndex = nextCharIndex;
48
+ } else {
49
+ var i = node.childNodes.length;
50
+ while (i--) {
51
+ nodeStack.push(node.childNodes[i]);
52
+ }
53
+ }
54
+ }
55
+
56
+ const sel = window.getSelection();
57
+ sel.removeAllRanges();
58
+ sel.addRange(range);
59
+ }
60
+
61
+ export {
62
+ saveSelection,
63
+ restoreSelection
64
+ }