Skip to content

Commit c219eb1

Browse files
feat: add component SkeletonLoading
Signed-off-by: Luka Trovic <luka@nextcloud.com>
1 parent e98eb90 commit c219eb1

File tree

4 files changed

+194
-2
lines changed

4 files changed

+194
-2
lines changed

src/components/Editor.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
:has-connection-issue="hasConnectionIssue"
3333
@reconnect="reconnect" />
3434

35+
<SkeletonLoading v-if="!contentLoaded" />
3536
<Wrapper v-if="displayed"
3637
:sync-error="syncError"
3738
:has-connection-issue="hasConnectionIssue"
@@ -118,10 +119,12 @@ import ContentContainer from './Editor/ContentContainer.vue'
118119
import Status from './Editor/Status.vue'
119120
import MainContainer from './Editor/MainContainer.vue'
120121
import Wrapper from './Editor/Wrapper.vue'
122+
import SkeletonLoading from './SkeletonLoading.vue'
121123
122124
export default {
123125
name: 'Editor',
124126
components: {
127+
SkeletonLoading,
125128
DocumentStatus,
126129
Wrapper,
127130
MainContainer,

src/components/Editor/Wrapper.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
<div class="text-editor__wrapper"
2525
:class="{
2626
'has-conflicts': hasSyncCollission,
27-
'icon-loading': !contentLoaded && !hasConnectionIssue,
2827
'is-rich-workspace': $isRichWorkspace,
2928
'is-rich-editor': $isRichEditor,
3029
'show-color-annotations': showAuthorAnnotations

src/components/SkeletonLoading.vue

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<template>
2+
<div class="placeholder-main placeholder-main-text">
3+
<!-- Placeholder animation -->
4+
<template v-for="(suffix, gradientIndex) in ['-regular', '-reverse']">
5+
<svg :key="'gradient' + suffix" :class="'placeholder-gradient placeholder-gradient' + suffix">
6+
<defs>
7+
<linearGradient :id="'placeholder-gradient' + suffix">
8+
<stop offset="0%" :stop-color="(gradientIndex === 0) ? colorPlaceholderLight : colorPlaceholderDark" />
9+
<stop offset="100%" :stop-color="(gradientIndex === 0) ? colorPlaceholderDark : colorPlaceholderLight" />
10+
</linearGradient>
11+
</defs>
12+
</svg>
13+
14+
<ul :key="'list' + suffix" :class="'placeholder-list placeholder-list' + suffix">
15+
<li v-for="(width, index) in placeholderData" :key="'placeholder' + suffix + index">
16+
<svg class="text-placeholder"
17+
xmlns="http://www.w3.org/2000/svg"
18+
:fill="'url(#placeholder-gradient' + suffix + ')'">
19+
<rect class="text-placeholder-line-one" :style="textPlaceholderData[0]" />
20+
<rect class="text-placeholder-line-two" :style="textPlaceholderData[1]" />
21+
<rect class="text-placeholder-line-three" :style="textPlaceholderData[2]" />
22+
<rect class="text-placeholder-line-four" :style="textPlaceholderData[3]" />
23+
</svg>
24+
</li>
25+
</ul>
26+
</template>
27+
</div>
28+
</template>
29+
30+
<script>
31+
const bodyStyles = window.getComputedStyle(document.body)
32+
const colorPlaceholderDark = bodyStyles.getPropertyValue('--color-placeholder-dark')
33+
const colorPlaceholderLight = bodyStyles.getPropertyValue('--color-placeholder-light')
34+
35+
export default {
36+
name: 'SkeletonLoading',
37+
38+
props: {
39+
count: {
40+
type: Number,
41+
default: 5,
42+
},
43+
},
44+
45+
setup() {
46+
return {
47+
colorPlaceholderDark,
48+
colorPlaceholderLight,
49+
}
50+
},
51+
52+
computed: {
53+
placeholderData() {
54+
const data = []
55+
for (let i = 0; i < this.count; i++) {
56+
// generate random widths
57+
data.push('width: ' + (Math.floor(Math.random() * 40) + 50) + '%')
58+
}
59+
return data
60+
},
61+
textPlaceholderData() {
62+
const data = []
63+
for (let i = 0; i < 4; i++) {
64+
// generate random widths
65+
data.push('width: ' + (Math.floor(Math.random() * 50) + 60) + '%')
66+
}
67+
return data
68+
},
69+
},
70+
}
71+
</script>
72+
73+
<style lang="scss" scoped>
74+
$clickable-area: 44px;
75+
$margin: 8px;
76+
$messages-list-max-width: 670px;
77+
78+
.placeholder-main {
79+
max-width: $messages-list-max-width;
80+
position: relative;
81+
margin-bottom: auto;
82+
z-index: 1;
83+
84+
&-text {
85+
margin: 50px auto 0;
86+
width: 100%;
87+
}
88+
}
89+
90+
#rich-workspace .placeholder-main-text {
91+
margin: 40px 0 0;
92+
}
93+
94+
.placeholder-list {
95+
position: absolute;
96+
transform: translateZ(0);
97+
}
98+
99+
.placeholder-list-regular {
100+
animation: pulse 2s;
101+
animation-iteration-count: infinite;
102+
animation-timing-function: linear;
103+
}
104+
105+
.placeholder-list-reverse {
106+
animation: pulse-reverse 2s;
107+
animation-iteration-count: infinite;
108+
animation-timing-function: linear;
109+
}
110+
111+
.placeholder-gradient {
112+
position: fixed;
113+
height: 0;
114+
width: 0;
115+
z-index: -1;
116+
}
117+
118+
.text-placeholder {
119+
width: min($messages-list-max-width, 100vw);
120+
height: 6em;
121+
margin: $margin auto;
122+
padding: 4px 8px 0 14px;
123+
display: block;
124+
125+
&-icon {
126+
width: $clickable-area;
127+
height: $clickable-area;
128+
cx: calc(#{$clickable-area} / 2);
129+
cy: calc(#{$clickable-area} / 2);
130+
r: calc(#{$clickable-area} / 2);
131+
}
132+
133+
&-line-one,
134+
&-line-two,
135+
&-line-three,
136+
&-line-four {
137+
width: 670px;
138+
height: 1em;
139+
}
140+
141+
&-line-one {
142+
y: 5px;
143+
y: 0.33em;
144+
width: 175px;
145+
}
146+
147+
&-line-two {
148+
y: 25px;
149+
y: 1.66em;
150+
}
151+
152+
&-line-three {
153+
y: 45px;
154+
y: 3em;
155+
}
156+
157+
&-line-four {
158+
y: 65px;
159+
y: 4.33em;
160+
}
161+
}
162+
163+
@keyframes pulse {
164+
0% {
165+
opacity: 1;
166+
}
167+
50% {
168+
opacity: 0;
169+
}
170+
100% {
171+
opacity: 1;
172+
}
173+
}
174+
175+
@keyframes pulse-reverse {
176+
0% {
177+
opacity: 0;
178+
}
179+
50% {
180+
opacity: 1;
181+
}
182+
100% {
183+
opacity: 0;
184+
}
185+
}
186+
187+
</style>

src/views/RichWorkspace.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
-->
2222

2323
<template>
24-
<div v-if="enabled" id="rich-workspace" :class="{'icon-loading': !loaded || !ready, 'focus': focus, 'dark': darkTheme, 'creatable': canCreate }">
24+
<div v-if="enabled" id="rich-workspace" :class="{'focus': focus, 'dark': darkTheme, 'creatable': canCreate }">
25+
<SkeletonLoading v-if="!loaded || !ready" />
2526
<Editor v-if="file"
2627
v-show="ready"
2728
:key="file.path"
@@ -44,13 +45,15 @@
4445
import axios from '@nextcloud/axios'
4546
import { generateOcsUrl } from '@nextcloud/router'
4647
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
48+
import SkeletonLoading from '../components/SkeletonLoading.vue'
4749
4850
const IS_PUBLIC = !!(document.getElementById('isPublic'))
4951
const WORKSPACE_URL = generateOcsUrl('apps/text' + (IS_PUBLIC ? '/public' : '') + '/workspace', 2)
5052
5153
export default {
5254
name: 'RichWorkspace',
5355
components: {
56+
SkeletonLoading,
5457
Editor: () => import(/* webpackChunkName: "editor" */'./../components/Editor.vue'),
5558
},
5659
props: {

0 commit comments

Comments
 (0)