summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-08-21 09:46:20 +0200
committerJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2020-08-21 17:25:14 +0200
commit417448cac5e1ebac691a8fc417a578f45e9bf08e (patch)
tree56c46b30a9a42dd721f8426b3faee4fd9a54b56d /src
parent68c5b1ce9c2739e2909dc32dbf8f302f52b7aacb (diff)
Implement masonry
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/components/ContactDetails.vue225
-rw-r--r--src/components/ContactDetails/ContactDetailsAddNewProp.vue2
-rw-r--r--src/components/ContactDetails/ContactDetailsProperty.vue45
-rw-r--r--src/components/ContactsList.vue3
-rw-r--r--src/components/Properties/PropertyDateTime.vue9
-rw-r--r--src/components/Properties/PropertyGroups.vue2
-rw-r--r--src/components/Properties/PropertyMultipleText.vue10
-rw-r--r--src/components/Properties/PropertySelect.vue8
-rw-r--r--src/components/Properties/PropertyText.vue11
9 files changed, 232 insertions, 83 deletions
diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue
index 58d68ff6..e806904a 100644
--- a/src/components/ContactDetails.vue
+++ b/src/components/ContactDetails.vue
@@ -183,60 +183,74 @@
<section v-if="loadingData" class="icon-loading contact-details" />
<!-- contact details -->
- <section v-else class="contact-details">
+ <section v-else
+ v-masonry="contactDetailsSelector"
+ class="contact-details"
+ :fit-width="true"
+ item-selector=".property-masonry"
+ :transition-duration="0">
<!-- properties iteration -->
<!-- using contact.key in the key and index as key to avoid conflicts between similar data and exact key -->
<!-- passing the debounceUpdateContact so that the contact-property component contains the function
and allow us to use it on the rfcProps since the scope is forwarded to the actions -->
- <ContactProperty v-for="(property, index) in sortedProperties"
- :key="`${index}-${contact.key}-${property.name}`"
- :index="index"
- :sorted-properties="sortedProperties"
- :property="property"
- :contact="contact"
- :local-contact="localContact"
- :update-contact="debounceUpdateContact" />
+ <div v-for="(properties, name) in groupedProperties"
+ :key="name"
+ v-masonry-tile
+ class="property-masonry">
+ <ContactProperty v-for="(property, index) in properties"
+ :key="`${index}-${contact.key}-${property.name}`"
+ :is-first-property="index===0"
+ :is-last-property="index === properties.length - 1"
+ :property="property"
+ :contact="contact"
+ :local-contact="localContact"
+ :update-contact="debounceUpdateContact" />
+ </div>
<!-- addressbook change select - no last property because class is not applied here,
empty property because this is a required prop on regular property-select. But since
we are hijacking this... (this is supposed to be used with a ICAL.property, but to avoid code
duplication, we created a fake propModel and property with our own options here) -->
- <PropertySelect v-if="addressbooksOptions.length > 1"
+ <PropertySelect v-masonry-tile
:prop-model="addressbookModel"
:value.sync="addressbook"
:is-first-property="true"
:is-last-property="true"
:property="{}"
- class="property--addressbooks property--last property--without-actions" />
+ class="property-masonry property--addressbooks property--last property--without-actions" />
<!-- Groups always visible -->
- <PropertyGroups :prop-model="groupsModel"
+ <PropertyGroups v-masonry-tile
+ :prop-model="groupsModel"
:value.sync="groups"
:contact="contact"
:is-read-only="isReadOnly"
- class="property--groups property--last" />
+ class="property-masonry property--groups property--last" />
+
+ <!-- new property select -->
+ <AddNewProp v-masonry-tile :contact="contact" class="property-masonry" />
<!-- Last modified-->
<PropertyRev v-if="contact.rev" :value="contact.rev" />
-
- <!-- new property select -->
- <AddNewProp v-if="!isReadOnly" :contact="contact" />
</section>
</template>
</div>
</template>
<script>
+import { showError } from '@nextcloud/dialogs'
+import { stringify } from 'ical.js'
import debounce from 'debounce'
import PQueue from 'p-queue'
import qr from 'qr-image'
-import { stringify } from 'ical.js'
-import Actions from '@nextcloud/vue/dist/Components/Actions'
-import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
+import Vue from 'vue'
+import { VueMasonryPlugin } from 'vue-masonry'
+
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
-import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
+import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
+import Actions from '@nextcloud/vue/dist/Components/Actions'
import Modal from '@nextcloud/vue/dist/Components/Modal'
-import { showError } from '@nextcloud/dialogs'
+import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import rfcProps from '../models/rfcProps'
import validate from '../services/validate'
@@ -249,6 +263,7 @@ import PropertyGroups from './Properties/PropertyGroups'
import PropertyRev from './Properties/PropertyRev'
import PropertySelect from './Properties/PropertySelect'
+Vue.use(VueMasonryPlugin)
const updateQueue = new PQueue({ concurrency: 1 })
export default {
@@ -297,6 +312,8 @@ export default {
qrcode: '',
showPickAddressbookModal: false,
pickedAddressbook: null,
+
+ contactDetailsSelector: '.contact-details',
}
},
@@ -356,6 +373,29 @@ export default {
},
/**
+ * Contact properties filtered and grouped by rfcProps.fieldOrder
+ *
+ * @returns {Object}
+ */
+ groupedProperties() {
+ return this.sortedProperties
+ .reduce((list, property) => {
+ // If there is no component to display this prop, ignore it
+ if (!this.canDisplay(property)) {
+ return list
+ }
+
+ // Init if needed
+ if (!list[property.name]) {
+ list[property.name] = []
+ }
+
+ list[property.name].push(property)
+ return list
+ }, {})
+ },
+
+ /**
* Fake model to use the propertySelect component
*
* @returns {Object}
@@ -442,9 +482,16 @@ export default {
if (this.contactKey && newContact !== oldContact) {
this.selectContact(this.contactKey)
}
+
+ // Reflow grid
+ this.$redrawVueMasonry(this.contactDetailsSelector)
},
},
+ updated() {
+ this.$redrawVueMasonry(this.contactDetailsSelector)
+ },
+
beforeMount() {
// load the desired data if we already selected a contact
if (this.contactKey) {
@@ -708,6 +755,142 @@ export default {
})
}
},
+
+ /**
+ * Should display the property
+ *
+ * @param {Property} property the property to check
+ * @returns {boolean}
+ */
+ canDisplay(property) {
+ const propModel = rfcProps.properties[property.name]
+ const propType = propModel && propModel.force
+ ? propModel.force
+ : property.getDefaultType()
+
+ return propModel && propType !== 'unknown'
+ },
},
}
</script>
+
+<style lang="scss">
+#contact-details {
+ flex: 1 1 100%;
+ min-width: 0;
+
+ // Header with avatar, name, position, actions...
+ header {
+ display: flex;
+ align-items: center;
+ padding: 50px 0 20px;
+ font-weight: bold;
+
+ // ORG-TITLE-NAME
+ #contact-header-infos {
+ display: flex;
+ flex: 1 1 auto; // shrink avatar before this one
+ flex-direction: column;
+ h2,
+ #details-org-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0;
+ }
+ input {
+ overflow: hidden;
+ flex: 1 1;
+ min-width: 100px;
+ max-width: 100%;
+ margin: 0;
+ padding: 4px 5px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ border: none;
+ background: transparent;
+ font-size: inherit;
+ &#contact-fullname {
+ font-weight: bold;
+ }
+ }
+ #contact-org:placeholder-shown {
+ max-width: 20%;
+ }
+ }
+
+ // ACTIONS
+ #contact-header-actions {
+ position: relative;
+ display: flex;
+ .header-menu {
+ margin-right: 10px;
+ }
+ .header-icon {
+ width: 44px;
+ height: 44px;
+ padding: 14px;
+ cursor: pointer;
+ opacity: .7;
+ border-radius: 22px;
+ background-size: 16px;
+ &:hover,
+ &:focus {
+ opacity: 1;
+ }
+ &.header-icon--pulse {
+ width: 16px;
+ height: 16px;
+ margin: 8px;
+ }
+ }
+ }
+ }
+
+ // List of all properties
+ section.contact-details {
+ margin: 0 auto;
+
+ .property-masonry {
+ width: 350px;
+ }
+
+ .property--rev {
+ position: absolute;
+ right: 22px;
+ bottom: 0;
+ height: 44px;
+ opacity: .5;
+ color: var(--color-text-lighter);
+ line-height: 44px;
+ }
+ }
+
+ #qrcode-modal {
+ .modal-container {
+ display: flex;
+ padding: 10px;
+ background-color: #fff;
+ .qrcode {
+ max-width: 100%;
+ }
+ }
+ }
+
+ #pick-addressbook-modal {
+ .modal-container {
+ display: flex;
+ overflow: visible;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ margin-bottom: 20px;
+ padding: 10px;
+ background-color: #fff;
+ .multiselect {
+ flex: 1 1 100%;
+ width: 100%;
+ margin-bottom: 20px;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/components/ContactDetails/ContactDetailsAddNewProp.vue b/src/components/ContactDetails/ContactDetailsAddNewProp.vue
index 7dc86f00..a4fe6b37 100644
--- a/src/components/ContactDetails/ContactDetailsAddNewProp.vue
+++ b/src/components/ContactDetails/ContactDetailsAddNewProp.vue
@@ -21,7 +21,7 @@
-->
<template>
- <div class="grid-span-3 property property--without-actions property--last">
+ <div class="property property--without-actions property--last">
<!-- title -->
<PropertyTitle :icon="'icon-add'" :readable-name="t('contacts', 'Add new property')" />
diff --git a/src/components/ContactDetails/ContactDetailsProperty.vue b/src/components/ContactDetails/ContactDetailsProperty.vue
index bcbfbe4e..ecc641cb 100644
--- a/src/components/ContactDetails/ContactDetailsProperty.vue
+++ b/src/components/ContactDetails/ContactDetailsProperty.vue
@@ -23,7 +23,6 @@
<template>
<!-- If not in the rfcProps then we don't want to display it -->
<component :is="componentInstance"
- v-if="propModel && propType !== 'unknown'"
ref="component"
:select-type.sync="selectType"
:prop-model="propModel"
@@ -31,7 +30,10 @@
:is-first-property="isFirstProperty"
:property="property"
:is-last-property="isLastProperty"
- :class="{'property--last': isLastProperty}"
+ :class="{
+ 'property--last': isLastProperty,
+ [`property-${propName}`]: true
+ }"
:local-contact="localContact"
:prop-name="propName"
:prop-type="propType"
@@ -59,16 +61,22 @@ export default {
type: Property,
default: true,
},
- sortedProperties: {
- type: Array,
- default() {
- return []
- },
+
+ /**
+ * Is it the first property of its kind
+ */
+ isFirstProperty: {
+ type: Boolean,
+ default: false,
},
- index: {
- type: Number,
- default: 0,
+ /**
+ * Is it the last property of its kind
+ */
+ isLastProperty: {
+ type: Boolean,
+ default: false,
},
+
contact: {
type: Contact,
default: null,
@@ -110,22 +118,6 @@ export default {
fieldOrder() {
return rfcProps.fieldOrder
},
-
- // is this the first property of its kind
- isFirstProperty() {
- if (this.index > 0) {
- return this.sortedProperties[this.index - 1].name.split('.').pop() !== this.propName
- }
- return true
- },
- // is this the last property of its kind
- isLastProperty() {
- // array starts at 0, length starts at 1
- if (this.index < this.sortedProperties.length - 1) {
- return this.sortedProperties[this.index + 1].name.split('.').pop() !== this.propName
- }
- return true
- },
isReadOnly() {
if (this.contact.addressbook) {
return this.contact.addressbook.readOnly
@@ -146,6 +138,7 @@ export default {
return this.property.name
},
+
/**
* Return the type or property
*
diff --git a/src/components/ContactsList.vue b/src/components/ContactsList.vue
index d89d493c..6548faf5 100644
--- a/src/components/ContactsList.vue
+++ b/src/components/ContactsList.vue
@@ -189,6 +189,9 @@ export default {
// Virtual scroller overrides
.vue-recycle-scroller {
position: sticky !important;
+ min-width: 0;
+ // try to take some width
+ flex: 1 1 300px;
}
.vue-recycle-scroller__item-view {
diff --git a/src/components/Properties/PropertyDateTime.vue b/src/components/Properties/PropertyDateTime.vue
index 7acc7233..3b2430f6 100644
--- a/src/components/Properties/PropertyDateTime.vue
+++ b/src/components/Properties/PropertyDateTime.vue
@@ -21,7 +21,7 @@
-->
<template>
- <div v-if="propModel" :class="`grid-span-${gridLength}`" class="property">
+ <div v-if="propModel" class="property">
<!-- title if first element -->
<PropertyTitle v-if="isFirstProperty && propModel.icon"
:icon="propModel.icon"
@@ -140,13 +140,6 @@ export default {
},
computed: {
- gridLength() {
- const hasTitle = this.isFirstProperty && this.propModel.icon ? 1 : 0
- const isLast = this.isLastProperty ? 1 : 0
- // length is always one & add one space at the end
- return hasTitle + 1 + isLast
- },
-
// make sure the property is valid
vcardTimeLocalValue() {
if (typeof this.localValue === 'string') {
diff --git a/src/components/Properties/PropertyGroups.vue b/src/components/Properties/PropertyGroups.vue
index bb1b03f2..170565e1 100644
--- a/src/components/Properties/PropertyGroups.vue
+++ b/src/components/Properties/PropertyGroups.vue
@@ -21,7 +21,7 @@
-->
<template>
- <div v-if="propModel" class="grid-span-2 property property--without-actions">
+ <div v-if="propModel" class="property property--without-actions">
<PropertyTitle
icon="icon-contacts"
:readable-name="t('contacts', 'Groups')" />
diff --git a/src/components/Properties/PropertyMultipleText.vue b/src/components/Properties/PropertyMultipleText.vue
index d23d9434..3b4be4c1 100644
--- a/src/components/Properties/PropertyMultipleText.vue
+++ b/src/components/Properties/PropertyMultipleText.vue
@@ -21,7 +21,7 @@
-->
<template>
- <div v-if="propModel" :class="`grid-span-${gridLength}`" class="property">
+ <div v-if="propModel" class="property">
<!-- title if first element -->
<PropertyTitle v-if="isFirstProperty && propModel.icon"
:icon="propModel.icon"
@@ -125,14 +125,6 @@ export default {
},
computed: {
- gridLength() {
- const hasTitle = this.isFirstProperty && this.propModel.icon ? 1 : 0
- const isLast = this.isLastProperty
- const hasValueNames = this.propModel.options || this.selectType || !this.property.isStructuredValue ? 1 : 0
- const length = this.propModel.displayOrder ? this.propModel.displayOrder.length : this.value.length
- return hasValueNames + hasTitle + length + isLast
- },
-
filteredValue() {
return this.localValue.filter((value, index) => index > 0)
},
diff --git a/src/components/Properties/PropertySelect.vue b/src/components/Properties/PropertySelect.vue
index 67b7a35c..8e4ef727 100644
--- a/src/components/Properties/PropertySelect.vue
+++ b/src/components/Properties/PropertySelect.vue
@@ -21,7 +21,7 @@
-->
<template>
- <div v-if="propModel" :class="`grid-span-${gridLength}`" class="property">
+ <div v-if="propModel" class="property">
<!-- title if first element -->
<PropertyTitle v-if="isFirstProperty && propModel.icon"
:icon="propModel.icon"
@@ -83,12 +83,6 @@ export default {
},
computed: {
- gridLength() {
- const hasTitle = this.isFirstProperty && this.propModel.icon ? 1 : 0
- const isLast = this.isLastProperty ? 1 : 0
- // length is one & add one space at the end
- return hasTitle + 1 + isLast
- },
// is there only one option available
isSingleOption() {
return this.propModel.options.length <= 1
diff --git a/src/components/Properties/PropertyText.vue b/src/components/Properties/PropertyText.vue
index 39554ef3..129bd811 100644
--- a/src/components/Properties/PropertyText.vue
+++ b/src/components/Properties/PropertyText.vue
@@ -21,7 +21,7 @@
-->
<template>
- <div v-if="propModel" :class="`grid-span-${gridLength} property-${propName}`" class="property">
+ <div v-if="propModel" class="property">
<!-- title if first element -->
<PropertyTitle v-if="isFirstProperty && propModel.icon"
:icon="propModel.icon"
@@ -131,15 +131,6 @@ export default {
},
computed: {
- gridLength() {
- const hasTitle = this.isFirstProperty && this.propModel.icon ? 1 : 0
- const isLast = this.isLastProperty ? 1 : 0
- const noteHeight = this.propName === 'note'
- ? this.noteHeight
- : 0
- // length is one & add one space at the end
- return hasTitle + 1 + isLast + noteHeight
- },
inputmode() {
if (this.propName === 'tel') {
return 'tel'