goodteditor-ui 1.0.10 → 1.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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import Avatar from './src/components/ui/Avatar.vue';
1
2
  import Badge from './src/components/ui/Badge.vue';
2
3
  import Collapse from './src/components/ui/Collapse.vue';
3
4
  import ColorPicker from './src/components/ui/ColorPicker.vue';
@@ -16,12 +17,14 @@ import Pagination from './src/components/ui/Pagination.vue';
16
17
  import Paginator from './src/components/ui/Paginator.vue';
17
18
  import Popover from './src/components/ui/Popover.vue';
18
19
  import Popup from './src/components/ui/Popup.vue';
20
+ import ResponsiveContainer from './src/components/ui/ResponsiveContainer.vue';
19
21
  import Select from './src/components/ui/Select.vue';
20
22
  import TimePicker from './src/components/ui/TimePicker.vue';
21
23
  import Tooltip from './src/components/ui/Tooltip.vue';
22
24
  import Grid from './src/components/ui/Grid.vue';
23
25
 
24
26
  export {
27
+ Avatar,
25
28
  Badge,
26
29
  Collapse,
27
30
  ColorPicker,
@@ -40,8 +43,9 @@ export {
40
43
  Paginator,
41
44
  Popover,
42
45
  Popup,
46
+ ResponsiveContainer,
43
47
  Select,
44
48
  TimePicker,
45
49
  Tooltip,
46
- Grid
50
+ Grid,
47
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goodteditor-ui",
3
- "version": "1.0.10",
3
+ "version": "1.0.13",
4
4
  "main": "index.js",
5
5
  "homepage": "https://goodt-ui.netlify.app/",
6
6
  "scripts": {
@@ -0,0 +1,68 @@
1
+ ```vue
2
+ <template>
3
+ <div class="pad-l5">
4
+ <div class="p">
5
+ <!-- default -->
6
+ <ui-avatar v-bind="props" @load="onLoad" @error="onError"></ui-avatar>
7
+
8
+ <!-- custom slots -->
9
+ <ui-avatar v-bind="props">
10
+ <template v-slot="{abbr}">
11
+ <code>{{abbr}}</code>
12
+ </template>
13
+ <template #loading>loading</template>
14
+ </ui-avatar>
15
+ </div>
16
+ <div class="row">
17
+ <div class="col">
18
+ <template v-for="key in Object.keys(props)">
19
+ <label class="text-small " v-if="isBool(key)">
20
+ <input class="checkbox checkbox-small" type="checkbox" v-model="props[key]">
21
+ <span class="v-mid">{{key}}</span>
22
+ </label>
23
+ <div :key="key" v-else>
24
+ <label class="text-xsmall">{{key}}</label>
25
+ <input class="input input-small d-block" type="text" v-model="props[key]">
26
+ </div>
27
+ </template>
28
+ </div>
29
+ <div class="col">
30
+ <pre class="text-xsmall" style="max-height:10rem">{{logStr}}</pre>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </template>
35
+ <script>
36
+ import UiAvatar from './Avatar.vue';
37
+
38
+ export default {
39
+ components: { UiAvatar },
40
+ data() {
41
+ return {
42
+ props: {
43
+ round: true,
44
+ src:'https://picsum.photos/150/150',
45
+ size: '4rem',
46
+ alt: 'John Wick',
47
+ color: '#ffffff',
48
+ background:'#fca522',
49
+ },
50
+ log:[]
51
+ }
52
+ },
53
+ computed: {
54
+ logStr() {
55
+ return this.log.reverse().join("\n")
56
+ }
57
+ },
58
+ methods: {
59
+ isBool(key) {
60
+ return key === 'round'
61
+ },
62
+ addLog(n,e) { this.log.push(`event: ${n}`) },
63
+ onLoad(e) { this.addLog('load') },
64
+ onError(e) { this.addLog('error') },
65
+ }
66
+ };
67
+ </script>
68
+ ```
@@ -0,0 +1,177 @@
1
+ <template>
2
+ <div class="ui-avatar" :style="cssStyle">
3
+ <img
4
+ class="ui-avatar__img"
5
+ :src="src"
6
+ :alt="isDoneState ? alt : ''"
7
+ @error="onImageError"
8
+ @load="onImageLoad"
9
+ v-if="isLoadingState || isDoneState"
10
+ />
11
+ <div class="ui-avatar__alt" v-if="isErrorState">
12
+ <!--
13
+ @slot Alternative content slot (displayed if no image provided)
14
+ @binding {string} alt alternative text
15
+ @binding {string} abbr alternative text abbr
16
+ -->
17
+ <slot v-bind="{ alt, abbr }">
18
+ <b class="ui-avatar__abbr">{{ abbr }}</b>
19
+ </slot>
20
+ </div>
21
+ <div class="ui-avatar__loading" v-else-if="isLoadingState">
22
+ <!--
23
+ @slot Loading content slot
24
+ -->
25
+ <slot name="loading">
26
+ <div class="preloader color-inherit"></div>
27
+ </slot>
28
+ </div>
29
+ </div>
30
+ </template>
31
+ <style lang="less" scoped>
32
+ .ui-avatar {
33
+ --size: 2rem;
34
+ --alt-scale: 0.4;
35
+ position: relative;
36
+ width: var(--size);
37
+ height: var(--size);
38
+ display: inline-flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ overflow: hidden;
42
+ &__abbr {
43
+ font-size: calc(var(--size) * var(--alt-scale));
44
+ line-height: 1;
45
+ }
46
+ &__img {
47
+ width: 100%;
48
+ height: 100%;
49
+ object-fit: cover;
50
+ }
51
+ &__alt,
52
+ &__loading {
53
+ position: absolute;
54
+ }
55
+ }
56
+ </style>
57
+ <script>
58
+ const State = {
59
+ LOADING: 'loading',
60
+ ERROR: 'error',
61
+ DONE: 'done',
62
+ };
63
+ export default {
64
+ props: {
65
+ /**
66
+ * Avatar src url
67
+ */
68
+ src: {
69
+ type: String,
70
+ default: '',
71
+ },
72
+ /**
73
+ * Alternative text
74
+ */
75
+ alt: {
76
+ type: String,
77
+ default: '',
78
+ },
79
+ /**
80
+ * Color (any web color format)
81
+ */
82
+ color: {
83
+ type: String,
84
+ default: '#fff',
85
+ },
86
+ /**
87
+ * Background color (any web color format)
88
+ */
89
+ background: {
90
+ type: String,
91
+ default: '#999',
92
+ },
93
+ /**
94
+ * Whether the avatar should be around
95
+ */
96
+ round: {
97
+ type: Boolean,
98
+ default: true,
99
+ },
100
+ /**
101
+ * Defines avatar's size (any web size)
102
+ */
103
+ size: {
104
+ type: String,
105
+ default: '2rem',
106
+ },
107
+ },
108
+ data: () => ({ state: State.LOADING }),
109
+ computed: {
110
+ /**
111
+ * @return {boolean}
112
+ */
113
+ isLoadingState() {
114
+ return this.state === State.LOADING;
115
+ },
116
+ /**
117
+ * @return {boolean}
118
+ */
119
+ isErrorState() {
120
+ return this.state === State.ERROR;
121
+ },
122
+ /**
123
+ * @return {boolean}
124
+ */
125
+ isDoneState() {
126
+ return this.state === State.DONE;
127
+ },
128
+ /**
129
+ * @return {object}
130
+ */
131
+ cssClass() {
132
+ return { 'ui-avatar--round': this.round };
133
+ },
134
+ /**
135
+ * @return {object}
136
+ */
137
+ cssStyle() {
138
+ const { size, color, round, background } = this;
139
+ const borderRadius = round ? '50%' : 0;
140
+ return { '--size': size, height: size, color, background, borderRadius };
141
+ },
142
+ /**
143
+ * @return {string}
144
+ */
145
+ abbr() {
146
+ const str = this.alt ?? '';
147
+ return str
148
+ .split(' ')
149
+ .map(s => (s.length ? s[0].toUpperCase() : ''))
150
+ .join('');
151
+ },
152
+ },
153
+ watch: {
154
+ src() {
155
+ this.state = State.LOADING;
156
+ },
157
+ },
158
+ methods: {
159
+ onImageError(event) {
160
+ this.state = State.ERROR;
161
+ /**
162
+ * Error event
163
+ * @property {Event} event
164
+ */
165
+ this.$emit('error', event);
166
+ },
167
+ onImageLoad(event) {
168
+ this.state = State.DONE;
169
+ /**
170
+ * Load event
171
+ * @property {Event} event
172
+ */
173
+ this.$emit('load', event);
174
+ },
175
+ },
176
+ };
177
+ </script>
@@ -19,10 +19,20 @@
19
19
  </style>
20
20
  <script>
21
21
  import { createPopper } from '@popperjs/core';
22
- import { Position } from './utils/Helpers';
22
+ import { Position, generateGetBoundingClientRect } from './utils/Helpers';
23
23
 
24
24
  const isElem = el => el instanceof HTMLElement;
25
25
 
26
+ const Strategy = Object.freeze({
27
+ ABSOLUTE: 'absolute',
28
+ FIXED: 'fixed'
29
+ });
30
+
31
+ const Modifier = Object.freeze({
32
+ FLIP: 'flip',
33
+ OFFSET: 'offset'
34
+ });
35
+
26
36
  export default {
27
37
  directives: {
28
38
  'append-body': {
@@ -96,6 +106,20 @@ export default {
96
106
  type: Boolean,
97
107
  default: false,
98
108
  },
109
+ /**
110
+ * Should follow the cursor
111
+ */
112
+ shouldFollowCursor: {
113
+ type: Boolean,
114
+ default: false
115
+ },
116
+ /**
117
+ * Cursor coordinates [ x, y ]
118
+ */
119
+ cursorCoordinates: {
120
+ type: Array,
121
+ default: () => [0, 0]
122
+ }
99
123
  },
100
124
  data() {
101
125
  return { cssStyle: {} };
@@ -114,39 +138,35 @@ export default {
114
138
  }
115
139
  return position;
116
140
  },
141
+ options() {
142
+ const { appendToBody, placement, positionFlip, positionOffset } = this;
143
+ const strategy = appendToBody ? Strategy.FIXED : Strategy.ABSOLUTE;
144
+ const modifiers = [
145
+ {
146
+ name: Modifier.FLIP,
147
+ enabled: positionFlip,
148
+ },
149
+ {
150
+ name: Modifier.OFFSET,
151
+ options: {
152
+ offset: positionOffset,
153
+ },
154
+ },
155
+ ]
156
+ return { placement, strategy, modifiers };
157
+ }
117
158
  },
118
159
  watch: {
119
160
  show: {
120
161
  handler(val) {
121
- const { appendToBody, placement, positionFlip, positionOffset } = this;
122
- const strategy = appendToBody ? 'fixed' : 'absolute';
123
162
  this.$nextTick(() => {
124
- if (val) {
163
+ if (val === true) {
125
164
  this.addEventListeners();
126
- this.popper = createPopper(this.getTarget(), this.$el, {
127
- placement,
128
- strategy,
129
- modifiers: [
130
- {
131
- name: 'flip',
132
- enabled: positionFlip,
133
- },
134
- {
135
- name: 'offset',
136
- options: {
137
- offset: positionOffset,
138
- },
139
- },
140
- ],
141
- });
142
- this.$nextTick(() => {
143
- this.calcStyle();
144
- this.popper.update();
145
- });
146
- } else {
147
- this.removeEventListeners();
148
- this.destroyPopper();
165
+ this.createPopper();
166
+ return;
149
167
  }
168
+
169
+ this.handleReset();
150
170
  });
151
171
  },
152
172
  immediate: true,
@@ -156,8 +176,7 @@ export default {
156
176
  this.popper = null;
157
177
  },
158
178
  destroyed() {
159
- this.removeEventListeners();
160
- this.destroyPopper();
179
+ this.handleReset();
161
180
  },
162
181
  methods: {
163
182
  addEventListeners() {
@@ -185,10 +204,40 @@ export default {
185
204
  setShow(val) {
186
205
  this.$nextTick(() => this.$emit('update:show', val));
187
206
  },
207
+ createPopper() {
208
+ const { shouldFollowCursor, options, $el: popper, cursorCoordinates } = this;
209
+ const target = this.getTarget();
210
+
211
+ if (shouldFollowCursor) {
212
+ const [x, y] = cursorCoordinates;
213
+ const virtualElem = {
214
+ getBoundingClientRect: generateGetBoundingClientRect(x, y),
215
+ contextElement: target
216
+ }
217
+
218
+ this.popper = createPopper(virtualElem, popper, options);
219
+ this.unwatchCursorCoordinates = this.$watch('cursorCoordinates', ([x, y]) => {
220
+ virtualElem.getBoundingClientRect = generateGetBoundingClientRect(x, y);
221
+ this.popper?.update();
222
+ })
223
+ } else {
224
+ this.popper = createPopper(target, popper, options);
225
+ }
226
+
227
+ this.$nextTick(() => {
228
+ this.calcStyle();
229
+ this.popper.update();
230
+ });
231
+ },
188
232
  destroyPopper() {
189
- this.popper && this.popper.destroy();
233
+ this.popper?.destroy();
190
234
  this.popper = null;
191
235
  },
236
+ handleReset() {
237
+ this.unwatchCursorCoordinates?.();
238
+ this.removeEventListeners();
239
+ this.destroyPopper();
240
+ },
192
241
  onWinBlur(e) {
193
242
  if (this.show) {
194
243
  this.setShow(false);
@@ -0,0 +1,58 @@
1
+ ```vue
2
+ <template>
3
+ <div class="pad-l5">
4
+ <div class="p">
5
+ <div class="form-label form-label-small">Tile width:</div>
6
+ <input class="input input-small" type="number" min="0" step="10" v-model.number="tileWidth">
7
+ </div>
8
+
9
+ <ui-responsive-container v-bind="binds" v-slot="{ breakpoint, width, height }">
10
+ <!-- {demo-tile} -->
11
+ <div class="tile" :class="[`tile--${breakpoint}`]" :style="tileStyle">
12
+ <div class="tile-body">
13
+ <div class="row">
14
+ <div class="col col-auto tile__avatar">
15
+ <img class="avatar avatar-4 round" src="https://picsum.photos/150/150" >
16
+ </div>
17
+ <div class="col">
18
+ <div class="text-bold">John Wick</div>
19
+ <div class="text-xsmall">john.wick@site.com</div>
20
+ <code>{{breakpoint}}</code> | <code>{{width}}&times;{{height}}</code>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ <!-- {/demo-tile} -->
25
+ </div>
26
+ </ui-responsive-container>
27
+ </div>
28
+ </template>
29
+ <script>
30
+ import UiResponsiveContainer from './ResponsiveContainer.vue';
31
+
32
+ export default {
33
+ components: { UiResponsiveContainer },
34
+ data: () => ({
35
+ binds: {
36
+ breakpoints: {
37
+ 's': 450,
38
+ 'm': 550,
39
+ 'l': 650,
40
+ }
41
+ },
42
+ tileWidth: ''
43
+ }),
44
+ computed: {
45
+ tileStyle() {
46
+ return { width: `${this.tileWidth}px` }
47
+ }
48
+ }
49
+ };
50
+ </script>
51
+ <style scoped>
52
+ .tile { min-width: 150px; }
53
+ .tile--s { color: red; }
54
+ .tile--s .tile__avatar { display:none; }
55
+ .tile--m { color: orange; }
56
+ .tile--l { color: green; }
57
+ </style>
58
+ ```
@@ -0,0 +1,99 @@
1
+ <script>
2
+ import { debounce } from './utils/Helpers';
3
+
4
+ const ObserverManager = {
5
+ observer: null,
6
+ records: new Map(),
7
+ register({ fn, el }) {
8
+ const { records } = this;
9
+ if (!records.size) {
10
+ this.createObserver();
11
+ }
12
+ records.set(el, fn);
13
+ this.observer.observe(el);
14
+ },
15
+ unregister(el) {
16
+ const { records } = this;
17
+ records.delete(el);
18
+ this.observer.unobserve(el);
19
+ if (!records.size) {
20
+ this.destroyObserver();
21
+ }
22
+ },
23
+ createObserver() {
24
+ const { records } = this;
25
+ const callback = entries => {
26
+ entries.forEach(entry => {
27
+ const fn = records.get(entry.target);
28
+ fn && fn(entry);
29
+ });
30
+ };
31
+ this.observer = new ResizeObserver(debounce(callback, 50));
32
+ },
33
+ destroyObserver() {
34
+ this.observer.disconnect();
35
+ this.observer = null;
36
+ },
37
+ };
38
+
39
+ export default {
40
+ props: {
41
+ /**
42
+ * Breakpoints hash { '<name>': '<max-width-in-px>' }<br>
43
+ * example:
44
+ * <pre>{ 'mobile': 400, 'tablet': 800 }</pre>
45
+ */
46
+ breakpoints: {
47
+ type: Object,
48
+ default: () => ({}),
49
+ },
50
+ },
51
+ data: () => ({ width: 0, height: 0 }),
52
+ computed: {
53
+ /**
54
+ * @return {[string, number][]}
55
+ */
56
+ breakpointEntries() {
57
+ return Object.entries(this.breakpoints).sort(([, aVal], [, bVal]) => aVal - bVal);
58
+ },
59
+ /**
60
+ * @return {string}
61
+ */
62
+ breakpoint() {
63
+ const { breakpointEntries, width } = this;
64
+ if (width === 0) {
65
+ return null;
66
+ }
67
+ const match = breakpointEntries.find(([, value]) => width < value);
68
+ return match?.[0] ?? null;
69
+ },
70
+ },
71
+ mounted() {
72
+ const { $el: el } = this;
73
+ const fn = ({ contentRect }) => {
74
+ this.width = contentRect.width | 0;
75
+ this.height = contentRect.height | 0;
76
+ };
77
+ ObserverManager.register({ fn, el });
78
+ },
79
+ beforeDestroy() {
80
+ ObserverManager.unregister(this.$el);
81
+ },
82
+ render(h) {
83
+ const { breakpoint, width, height } = this;
84
+ /*
85
+ @slot Page data slot
86
+ @binding {string} breakpoint breakpoint name
87
+ @binding {number} width current container width (px)
88
+ @binding {number} height current container height (px)
89
+ */
90
+ const content = this.$scopedSlots.default
91
+ ? this.$scopedSlots.default({ breakpoint, width, height })
92
+ : null;
93
+ if (content.length > 1) {
94
+ return h('div', 'Slot content should have one child');
95
+ }
96
+ return content;
97
+ },
98
+ };
99
+ </script>
@@ -1,3 +1,5 @@
1
+ Simple example
2
+
1
3
  ```vue
2
4
  <template>
3
5
  <div class="pos-rel pad-l5">
@@ -50,3 +52,63 @@ export default {
50
52
  };
51
53
  </script>
52
54
  ```
55
+
56
+ Advanced example. Using cursor tracking
57
+
58
+ ```vue
59
+ <template>
60
+ <div class="pos-rel pad-l5">
61
+ <ui-tooltip v-bind="options">
62
+ <template #target="{ binds }">
63
+ <div
64
+ ref="target"
65
+ class="pad-l2 bg-green color-white radius text-center"
66
+ v-bind="binds"
67
+ >
68
+ hover me
69
+ </div>
70
+ </template>
71
+ <div>
72
+ I follow the cursor everywhere
73
+ </div>
74
+ </ui-tooltip>
75
+ </div>
76
+ </template>
77
+ <script>
78
+ import UiTooltip from './Tooltip.vue';
79
+ export default {
80
+ components: { UiTooltip },
81
+ data() {
82
+ return {
83
+ options: {
84
+ show: false,
85
+ shouldFollowCursor: true,
86
+ cursorCoordinates: [0, 0]
87
+ }
88
+ };
89
+ },
90
+ mounted() {
91
+ this.$refs.target.addEventListener('mouseover', this.showTooltip);
92
+ this.$refs.target.addEventListener('mousemove', this.changeTooltipCoords);
93
+ this.$refs.target.addEventListener('mouseleave', this.hideTooltip);
94
+ },
95
+ beforeDestroy() {
96
+ this.$refs.target.removeEventListener('mouseover', this.showTooltip);
97
+ this.$refs.target.removeEventListener('mousemove', this.changeTooltipCoords);
98
+ this.$refs.target.removeEventListener('mouseleave', this.hideTooltip);
99
+
100
+ },
101
+ methods: {
102
+ showTooltip() {
103
+ this.options.show = true;
104
+ },
105
+ hideTooltip() {
106
+ this.options.show = false;
107
+ },
108
+ changeTooltipCoords({ clientX: x, clientY: y }) {
109
+ this.options.cursorCoordinates = [x, y];
110
+ }
111
+ }
112
+ };
113
+ </script>
114
+ ```
@@ -58,4 +58,27 @@ const useIntersectionObserver = (target, callback, options) => {
58
58
  return { observer, stop: () => observer.disconnect() };
59
59
  };
60
60
 
61
- export { scrollIntoView, isDateValid, nextId, Key, Position, debounce, useIntersectionObserver };
61
+ /**
62
+ * @param {number} x - x-axis coordinate
63
+ * @param {number} y - y-axis coordinate
64
+ * @return {function: DOMRect}
65
+ */
66
+ const generateGetBoundingClientRect = (x = 0, y = 0) => () => ({
67
+ width: 0,
68
+ height: 0,
69
+ top: y,
70
+ right: x,
71
+ bottom: y,
72
+ left: x,
73
+ });
74
+
75
+ export {
76
+ scrollIntoView,
77
+ isDateValid,
78
+ nextId,
79
+ Key,
80
+ Position,
81
+ debounce,
82
+ useIntersectionObserver,
83
+ generateGetBoundingClientRect,
84
+ };
@@ -43,6 +43,20 @@ export default {
43
43
  type: Boolean,
44
44
  default: false,
45
45
  },
46
+ /**
47
+ * Should follow the cursor
48
+ */
49
+ shouldFollowCursor: {
50
+ type: Boolean,
51
+ default: false
52
+ },
53
+ /**
54
+ * Cursor coordinates [ x, y ]
55
+ */
56
+ cursorCoordinates: {
57
+ type: Array,
58
+ default: () => [0, 0]
59
+ }
46
60
  },
47
61
  data() {
48
62
  return {
@@ -62,6 +76,8 @@ export default {
62
76
  positionOffset,
63
77
  autoWidth,
64
78
  popoverTarget: target,
79
+ shouldFollowCursor,
80
+ cursorCoordinates
65
81
  } = this;
66
82
  return {
67
83
  zIndex,
@@ -70,6 +86,8 @@ export default {
70
86
  positionOffset,
71
87
  autoWidth,
72
88
  target,
89
+ shouldFollowCursor,
90
+ cursorCoordinates
73
91
  };
74
92
  },
75
93
  },