@operato/contact 1.1.52 → 1.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/demo/ox-pfp.html +45 -0
  3. package/demo/pfp-edit.html +59 -0
  4. package/demo/pfp-view.html +46 -0
  5. package/dist/src/index.d.ts +2 -0
  6. package/dist/src/index.js +2 -0
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/ox-contact copy.d.ts +38 -1
  9. package/dist/src/ox-contact copy.js +66 -2
  10. package/dist/src/ox-contact copy.js.map +1 -1
  11. package/dist/src/ox-contact-edit.d.ts +3 -0
  12. package/dist/src/ox-contact-edit.js +9 -16
  13. package/dist/src/ox-contact-edit.js.map +1 -1
  14. package/dist/src/ox-contact-view.d.ts +1 -0
  15. package/dist/src/ox-contact-view.js +5 -16
  16. package/dist/src/ox-contact-view.js.map +1 -1
  17. package/dist/src/ox-contact.d.ts +2 -0
  18. package/dist/src/ox-contact.js.map +1 -1
  19. package/dist/src/ox-pfp-edit.d.ts +28 -0
  20. package/dist/src/ox-pfp-edit.js +314 -0
  21. package/dist/src/ox-pfp-edit.js.map +1 -0
  22. package/dist/src/ox-pfp-editor copy.d.ts +11 -0
  23. package/dist/src/ox-pfp-editor copy.js +71 -0
  24. package/dist/src/ox-pfp-editor copy.js.map +1 -0
  25. package/dist/src/ox-pfp-editor.d.ts +20 -0
  26. package/dist/src/ox-pfp-editor.js +165 -0
  27. package/dist/src/ox-pfp-editor.js.map +1 -0
  28. package/dist/src/ox-pfp-view.d.ts +15 -0
  29. package/dist/src/ox-pfp-view.js +100 -0
  30. package/dist/src/ox-pfp-view.js.map +1 -0
  31. package/dist/src/ox-pfp-viewer.d.ts +14 -0
  32. package/dist/src/ox-pfp-viewer.js +63 -0
  33. package/dist/src/ox-pfp-viewer.js.map +1 -0
  34. package/dist/src/ox-pfp.d.ts +14 -0
  35. package/dist/src/ox-pfp.js +104 -0
  36. package/dist/src/ox-pfp.js.map +1 -0
  37. package/dist/stories/index.stories copy.d.ts +24 -0
  38. package/dist/stories/index.stories copy.js +79 -0
  39. package/dist/stories/index.stories copy.js.map +1 -0
  40. package/dist/stories/ox-contact.stories.d.ts +24 -0
  41. package/dist/stories/ox-contact.stories.js +79 -0
  42. package/dist/stories/ox-contact.stories.js.map +1 -0
  43. package/dist/stories/ox-pfp.stories.d.ts +28 -0
  44. package/dist/stories/ox-pfp.stories.js +37 -0
  45. package/dist/stories/ox-pfp.stories.js.map +1 -0
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +14 -5
  48. package/src/index.ts +2 -0
  49. package/src/ox-contact-edit.ts +8 -16
  50. package/src/ox-contact-view.ts +6 -16
  51. package/src/ox-contact.ts +2 -0
  52. package/src/ox-pfp-edit.ts +351 -0
  53. package/src/ox-pfp-view.ts +98 -0
  54. package/src/ox-pfp.ts +97 -0
  55. package/stories/{index.stories.ts → ox-contact.stories.ts} +0 -0
  56. package/stories/ox-pfp.stories.ts +53 -0
@@ -1,3 +1,5 @@
1
+ import './ox-pfp-view.js'
2
+
1
3
  import { html, css, LitElement } from 'lit'
2
4
  import { customElement, property } from 'lit/decorators.js'
3
5
  import { Contact, ContactField, ContactItem } from './ox-contact'
@@ -60,23 +62,10 @@ export class OxContactView extends LitElement {
60
62
  font-size: 1.4em;
61
63
  }
62
64
 
63
- :host > div[name] > [monogram] {
65
+ :host ox-pfp-view {
64
66
  display: inline-block;
65
67
  width: 100px;
66
68
  height: 100px;
67
- line-height: 100px;
68
- border-radius: 999px;
69
- border: 1px solid darkgray;
70
- box-sizing: border-box;
71
- padding: 0;
72
- font-size: 40px;
73
- font-family: system-ui;
74
- overflow: hidden;
75
- text-align: center;
76
- vertical-align: center;
77
- background-color: gray;
78
- color: white;
79
- text-transform: uppercase;
80
69
  }
81
70
 
82
71
  :host > div[name] > div[name-content] {
@@ -90,7 +79,7 @@ export class OxContactView extends LitElement {
90
79
  @property({ type: Object }) contact: Contact = {}
91
80
 
92
81
  render() {
93
- const { items = [], note } = this.contact || {}
82
+ const { profile, items = [], note } = this.contact || {}
94
83
  var sorted: { [type: string]: ContactItem[] } = {}
95
84
 
96
85
  items.forEach(item => {
@@ -106,7 +95,8 @@ export class OxContactView extends LitElement {
106
95
 
107
96
  return html`
108
97
  <div name>
109
- <div monogram>${names[0]?.value.replace(/\s/g, '').slice(0, 2)}</div>
98
+ <ox-pfp-view .profile=${profile} .name=${names[0]?.value}></ox-pfp-view>
99
+
110
100
  <div name-content>
111
101
  ${names.map(
112
102
  item =>
package/src/ox-contact.ts CHANGED
@@ -2,6 +2,7 @@ import { html, css, LitElement } from 'lit'
2
2
  import { customElement, property, state } from 'lit/decorators.js'
3
3
  import './ox-contact-edit'
4
4
  import './ox-contact-view'
5
+ import { Profile } from './ox-pfp-view.js'
5
6
 
6
7
  export enum ContactField {
7
8
  Name = 'name',
@@ -32,6 +33,7 @@ export type ContactItem = {
32
33
  }
33
34
 
34
35
  export type Contact = {
36
+ profile?: Profile
35
37
  items?: ContactItem[]
36
38
  note?: string
37
39
  }
@@ -0,0 +1,351 @@
1
+ import { html, css, LitElement, PropertyValues } from 'lit'
2
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
+ import { Profile } from './ox-pfp-view'
4
+ import { FileDropHelper } from '@operato/utils'
5
+
6
+ function getPoint(e: Event): { x: number; y: number } | undefined {
7
+ if ((e as MouseEvent).button == 0) {
8
+ return {
9
+ x: (e as MouseEvent).clientX,
10
+ y: (e as MouseEvent).clientY
11
+ }
12
+ } else if ((e as TouchEvent).touches?.length == 1) {
13
+ const touch = (e as TouchEvent).touches[0]
14
+ return {
15
+ x: touch.clientX,
16
+ y: touch.clientY
17
+ }
18
+ } else {
19
+ return
20
+ }
21
+ }
22
+
23
+ @customElement('ox-pfp-edit')
24
+ export class OxPfpEdit extends LitElement {
25
+ static styles = css`
26
+ :host {
27
+ display: inline-block;
28
+ overflow: hidden;
29
+ }
30
+
31
+ [content] {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ width: 100%;
36
+ height: 100%;
37
+ }
38
+
39
+ [circle] {
40
+ width: 90%;
41
+ height: 90%;
42
+ position: relative;
43
+ }
44
+
45
+ [circle]::after {
46
+ position: absolute;
47
+ content: '';
48
+ width: 100%;
49
+ height: 100%;
50
+ box-shadow: 0 0 0 1000px lightgray;
51
+ border-radius: 50%;
52
+ opacity: 0.9;
53
+ }
54
+
55
+ [circle]::before {
56
+ content: '';
57
+ display: block;
58
+ position: absolute;
59
+ text-align: left;
60
+ line-height: 24px;
61
+ top: 0;
62
+ right: 0;
63
+ width: 100%;
64
+ height: 100%;
65
+ z-index: -1;
66
+ background-repeat: no-repeat;
67
+ background-image: var(
68
+ --background-image,
69
+ linear-gradient(var(--primary-color, lightgray), var(--primary-color, lightgray))
70
+ );
71
+ transform: var(--image-transform, '');
72
+ background-size: 100% 100%;
73
+ }
74
+
75
+ [monogram] {
76
+ width: 100%;
77
+ height: 100%;
78
+ border-radius: 50%;
79
+ border: 1px solid darkgray;
80
+ box-sizing: border-box;
81
+ padding: 0;
82
+ overflow: hidden;
83
+ background-color: gray;
84
+ color: white;
85
+
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ }
90
+
91
+ [monogram] div {
92
+ flex: 1;
93
+ text-transform: uppercase;
94
+ font-size: 2em;
95
+ font-family: system-ui;
96
+ text-align: center;
97
+ vertical-align: center;
98
+ }
99
+
100
+ [file-load] {
101
+ position: absolute;
102
+ left: 0;
103
+ top: 0;
104
+
105
+ display: flex;
106
+ place-content: center;
107
+ }
108
+
109
+ [file-load]:hover {
110
+ opacity: 50%;
111
+ }
112
+
113
+ #input-file {
114
+ display: none;
115
+ }
116
+
117
+ label {
118
+ display: flex;
119
+ align-content: center;
120
+ }
121
+
122
+ mwc-icon {
123
+ align-self: center;
124
+
125
+ color: var(--file-uploader-icon-color, black);
126
+ --mdc-icon-size: var(--file-uploader-icon-size, 20px);
127
+ }
128
+ `
129
+
130
+ @property({ type: Object }) profile: Profile = {}
131
+ @property({ type: String }) name?: string
132
+
133
+ @query('[circle]') circle!: HTMLDivElement
134
+
135
+ private dragStart?: { x: number; y: number }
136
+ private dragEndHandler = this.onDragEnd.bind(this) as EventListener
137
+ private dragMoveHandler = this.onDragMove.bind(this) as EventListener
138
+ private dragStartHandler = this.onDragStart.bind(this) as EventListener
139
+ private wheelEventHandler = this.onWheelEvent.bind(this) as EventListener
140
+ private dropFileHandler = this.onDropFile.bind(this) as EventListener
141
+
142
+ connectedCallback(): void {
143
+ super.connectedCallback()
144
+
145
+ FileDropHelper.set(this)
146
+
147
+ this.addEventListener('mousewheel', this.wheelEventHandler, false)
148
+
149
+ this.addEventListener('mousedown', this.dragStartHandler)
150
+ this.addEventListener('touchstart', this.dragStartHandler)
151
+
152
+ document.addEventListener('mouseup', this.dragEndHandler)
153
+ document.addEventListener('touchend', this.dragEndHandler)
154
+ document.addEventListener('touchcancel', this.dragEndHandler)
155
+ document.addEventListener('mousemove', this.dragMoveHandler)
156
+ document.addEventListener('touchmove', this.dragMoveHandler)
157
+
158
+ this.addEventListener('file-drop', this.dropFileHandler)
159
+ }
160
+
161
+ disconnectedCallback() {
162
+ this.removeEventListener('mousewheel', this.wheelEventHandler, false)
163
+
164
+ this.removeEventListener('mousedown', this.dragStartHandler!)
165
+ this.removeEventListener('touchstart', this.dragStartHandler!)
166
+
167
+ document.removeEventListener('mouseup', this.dragEndHandler!)
168
+ document.removeEventListener('touchend', this.dragEndHandler!)
169
+ document.removeEventListener('touchcancel', this.dragEndHandler!)
170
+ document.removeEventListener('mousemove', this.dragMoveHandler!)
171
+ document.removeEventListener('touchmove', this.dragMoveHandler!)
172
+
173
+ this.removeEventListener('file-drop', this.dropFileHandler)
174
+
175
+ super.disconnectedCallback()
176
+ }
177
+
178
+ render() {
179
+ return html`
180
+ <div content>
181
+ ${this.profile?.picture
182
+ ? html`<div circle></div>`
183
+ : html`<div monogram><div>${this.name?.replace(/\s/g, '').slice(0, 2)}</div></div>`}
184
+ </div>
185
+
186
+ <div file-load>
187
+ <label>
188
+ <input
189
+ id="input-file"
190
+ type="file"
191
+ accept="image/*"
192
+ hidden
193
+ capture="environment"
194
+ @change=${(e: Event) => this.onChangeFile(e)}
195
+ />
196
+ <mwc-icon>image_search</mwc-icon>
197
+ </label>
198
+ </div>
199
+
200
+ <slot></slot>
201
+ `
202
+ }
203
+
204
+ updated(changes: PropertyValues<this>) {
205
+ const { picture, zoom = 1, left = 0, top = 0 } = this.profile
206
+
207
+ if (picture) {
208
+ const style = this.style
209
+
210
+ style.setProperty('--background-image', `url(${picture})`)
211
+ style.setProperty('--image-transform', `translate(${left}%, ${top}%) scale(${zoom}, ${zoom})`)
212
+ }
213
+ }
214
+
215
+ get value() {
216
+ return this.profile
217
+ }
218
+
219
+ set value(value: Profile) {
220
+ this.profile = value
221
+ }
222
+
223
+ onWheelEvent(e: Event) {
224
+ e.preventDefault()
225
+
226
+ var delta = Math.max(-1, Math.min(1, (e as WheelEvent).deltaY || -(e as WheelEvent).detail))
227
+
228
+ const { top = 0, left = 0, zoom = 1, picture } = this.profile
229
+ this.profile = {
230
+ top,
231
+ left,
232
+ zoom: zoom * (1 - delta / 20),
233
+ picture
234
+ }
235
+
236
+ this.dispatchEvent(
237
+ new CustomEvent('change', {
238
+ detail: this.profile
239
+ })
240
+ )
241
+ }
242
+
243
+ onDragStart(e: Event) {
244
+ const point = getPoint(e)
245
+
246
+ if (point) {
247
+ this.dragStart = point
248
+ e.stopPropagation()
249
+ return false
250
+ }
251
+ }
252
+
253
+ onDragMove(e: Event) {
254
+ if (!this.dragStart) {
255
+ return false
256
+ }
257
+
258
+ const point = getPoint(e)
259
+
260
+ if (!point) {
261
+ return false
262
+ }
263
+
264
+ e.stopPropagation()
265
+ e.preventDefault()
266
+
267
+ const dragStart = point
268
+ var { x, y } = point
269
+
270
+ x -= this.dragStart.x
271
+ y -= this.dragStart.y
272
+
273
+ this.dragStart = dragStart
274
+
275
+ const { top = 0, left = 0, zoom = 1, picture } = this.profile
276
+ const { offsetHeight, offsetWidth } = this
277
+
278
+ this.profile = {
279
+ top: top + (y / offsetHeight) * 100,
280
+ left: left + (x / offsetWidth) * 100,
281
+ zoom,
282
+ picture
283
+ }
284
+
285
+ this.dispatchEvent(
286
+ new CustomEvent('change', {
287
+ detail: this.profile
288
+ })
289
+ )
290
+
291
+ return false
292
+ }
293
+
294
+ onDragEnd(e: Event) {
295
+ if (this.dragStart) {
296
+ e.stopPropagation()
297
+ e.preventDefault()
298
+
299
+ delete this.dragStart
300
+ }
301
+ }
302
+
303
+ onDropFile(e: Event) {
304
+ const dropFiles = (e as CustomEvent).detail
305
+ const imageFile = dropFiles[0]
306
+ if (!imageFile) {
307
+ return
308
+ }
309
+
310
+ const picture = URL.createObjectURL(imageFile)
311
+
312
+ this.profile = {
313
+ left: 0,
314
+ top: 0,
315
+ zoom: 1,
316
+ picture
317
+ }
318
+ }
319
+
320
+ onChangeFile(e: Event) {
321
+ const fileInput = e.currentTarget as HTMLInputElement
322
+ const imageFile = [...Array.from(fileInput.files as FileList)][0]
323
+ if (!imageFile) {
324
+ return
325
+ }
326
+
327
+ const picture = URL.createObjectURL(imageFile)
328
+
329
+ this.profile = {
330
+ left: 0,
331
+ top: 0,
332
+ zoom: 1,
333
+ picture
334
+ }
335
+
336
+ fileInput.files = null
337
+ }
338
+
339
+ fit() {
340
+ this.profile = {
341
+ ...this.profile,
342
+ left: 0,
343
+ top: 0,
344
+ zoom: 1
345
+ }
346
+ }
347
+
348
+ clear() {
349
+ this.profile = {}
350
+ }
351
+ }
@@ -0,0 +1,98 @@
1
+ import { html, css, LitElement, PropertyValues } from 'lit'
2
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
+
4
+ export type Profile = {
5
+ left?: number
6
+ top?: number
7
+ zoom?: number
8
+ picture?: string
9
+ }
10
+
11
+ @customElement('ox-pfp-view')
12
+ export class OxPfpView extends LitElement {
13
+ static styles = css`
14
+ :host {
15
+ display: inline-block;
16
+ }
17
+
18
+ [circle] {
19
+ width: 100%;
20
+ height: 100%;
21
+ border-radius: 50%;
22
+ position: relative;
23
+ background-color: lightgray;
24
+
25
+ overflow: hidden;
26
+ }
27
+
28
+ [circle]::before {
29
+ content: '';
30
+ display: block;
31
+ position: absolute;
32
+ text-align: left;
33
+ line-height: 24px;
34
+ top: 0;
35
+ right: 0;
36
+ width: 100%;
37
+ height: 100%;
38
+ background-repeat: no-repeat;
39
+ background-image: var(
40
+ --background-image,
41
+ linear-gradient(var(--primary-color, gray), var(--primary-color, gray))
42
+ );
43
+ transform: var(--image-transform, '');
44
+ background-size: 100% 100%;
45
+ }
46
+
47
+ [monogram] {
48
+ width: 100%;
49
+ height: 100%;
50
+ border-radius: 50%;
51
+ border: 1px solid darkgray;
52
+ box-sizing: border-box;
53
+ padding: 0;
54
+ overflow: hidden;
55
+ background-color: gray;
56
+ color: white;
57
+
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ }
62
+
63
+ [monogram] div {
64
+ flex: 1;
65
+ text-transform: uppercase;
66
+ font-size: 2em;
67
+ font-family: system-ui;
68
+ text-align: center;
69
+ vertical-align: center;
70
+ }
71
+ `
72
+
73
+ @property({ type: Object }) profile: Profile = {}
74
+ @property({ type: String }) name?: string
75
+
76
+ @query('[circle]') circle!: HTMLDivElement
77
+
78
+ render() {
79
+ return html`
80
+ ${this.profile?.picture
81
+ ? html`<div circle></div>`
82
+ : html`<div monogram><div>${this.name?.replace(/\s/g, '').slice(0, 2)}</div></div>`}
83
+
84
+ <slot></slot>
85
+ `
86
+ }
87
+
88
+ updated(changes: PropertyValues<this>) {
89
+ const { picture, zoom = 1, left = 0, top = 0 } = this.profile || {}
90
+
91
+ if (picture) {
92
+ const style = this.style
93
+
94
+ style.setProperty('--background-image', `url(${picture})`)
95
+ style.setProperty('--image-transform', `translate(${left}%, ${top}%) scale(${zoom}, ${zoom})`)
96
+ }
97
+ }
98
+ }
package/src/ox-pfp.ts ADDED
@@ -0,0 +1,97 @@
1
+ import './ox-pfp-edit.js'
2
+ import './ox-pfp-view.js'
3
+
4
+ import '@material/mwc-icon-button'
5
+
6
+ import { html, css, LitElement } from 'lit'
7
+ import { customElement, property, query, state } from 'lit/decorators.js'
8
+ import { Profile } from './ox-pfp-view.js'
9
+ import { OxPfpEdit } from './ox-pfp-edit.js'
10
+
11
+ @customElement('ox-pfp')
12
+ export class OxPfp extends LitElement {
13
+ static styles = css`
14
+ :host {
15
+ display: block;
16
+
17
+ --mdc-icon-button-size: 20px;
18
+ --mdc-icon-size: 20px;
19
+ }
20
+
21
+ ox-pfp-edit,
22
+ ox-pfp-view {
23
+ position: relative;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+
28
+ [buttons] {
29
+ position: absolute;
30
+ bottom: 0;
31
+ right: 0;
32
+ display: flex;
33
+ }
34
+
35
+ mwc-icon-button {
36
+ position: absolute;
37
+ }
38
+
39
+ mwc-icon-button[icon='fit_screen'] {
40
+ bottom: 0;
41
+ left: 0;
42
+ }
43
+
44
+ mwc-icon-button[icon='save'] {
45
+ bottom: 0;
46
+ right: 0;
47
+ }
48
+
49
+ mwc-icon-button[icon='edit'] {
50
+ bottom: 0;
51
+ right: 0;
52
+ }
53
+
54
+ mwc-icon-button[icon='cancel'] {
55
+ top: 0;
56
+ right: 0;
57
+ }
58
+
59
+ mwc-icon-button[icon='clear'] {
60
+ top: 0;
61
+ right: 16px;
62
+ }
63
+ `
64
+
65
+ @property({ type: Object }) profile: Profile = {}
66
+ @property({ type: String }) name?: string
67
+
68
+ @state() editMode?: boolean = false
69
+
70
+ @query('ox-pfp-edit') edit!: OxPfpEdit
71
+
72
+ render() {
73
+ return this.editMode
74
+ ? html`<ox-pfp-edit .profile=${this.profile} name=${this.name || ''}>
75
+ <mwc-icon-button icon="fit_screen" @click=${() => this.edit.fit()}></mwc-icon-button>
76
+ <mwc-icon-button icon="clear" @click=${() => this.edit.clear()}></mwc-icon-button>
77
+ <mwc-icon-button
78
+ icon="save"
79
+ @click=${() => {
80
+ this.profile = this.edit.profile
81
+ this.editMode = false
82
+ this.dispatchEvent(
83
+ new CustomEvent('change', {
84
+ detail: this.profile
85
+ })
86
+ )
87
+ }}
88
+ ></mwc-icon-button>
89
+ <mwc-icon-button icon="cancel" @click=${() => (this.editMode = false)}></mwc-icon-button>
90
+ </ox-pfp-edit> `
91
+ : html`
92
+ <ox-pfp-view .profile=${this.profile} name=${this.name || ''}>
93
+ <mwc-icon-button icon="edit" @click=${() => (this.editMode = true)}></mwc-icon-button>
94
+ </ox-pfp-view>
95
+ `
96
+ }
97
+ }