@c4h/chuci 0.1.0 → 0.2.1

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.
@@ -1,279 +1,279 @@
1
- import { ChuciElement } from '@/utils/base-element'
2
-
3
- export abstract class CcViewerBase extends ChuciElement {
4
- private _showPrevButton = true
5
- private _showNextButton = true
6
- protected isShow = false
7
- protected isLoading = false
8
-
9
- get showPrevButton() {
10
- return this._showPrevButton
11
- }
12
-
13
- set showPrevButton(value: boolean) {
14
- this._showPrevButton = value
15
- this.updateNavigationVisibility()
16
- }
17
-
18
- get showNextButton() {
19
- return this._showNextButton
20
- }
21
-
22
- set showNextButton(value: boolean) {
23
- this._showNextButton = value
24
- this.updateNavigationVisibility()
25
- }
26
-
27
- // Template method pattern - subclasses implement these
28
- protected abstract doOpen(url: string): void | Promise<void>
29
- protected abstract doClose(): void
30
- protected abstract getViewerContent(): string
31
-
32
- // Common lifecycle methods
33
- open(url: string): void {
34
- this.isShow = true
35
- this.isLoading = true
36
-
37
- // Call subclass implementation first
38
- const openPromise = Promise.resolve(this.doOpen(url))
39
-
40
- // Use microtask to ensure doOpen runs first (even if synchronous)
41
- Promise.resolve().then(() => {
42
- // Initial render for loading state
43
- this.render()
44
- })
45
-
46
- // Update after loading completes
47
- openPromise.then(() => {
48
- this.isLoading = false
49
- this.render()
50
- }).catch(_error => {
51
- this.isLoading = false
52
- this.render()
53
- })
54
- }
55
-
56
- close(): void {
57
- this.cleanupNavigationListeners()
58
- this.doClose()
59
- this.isShow = false
60
- this.isLoading = false
61
- this.render()
62
- this.dispatch('close')
63
- }
64
-
65
- protected cleanupNavigationListeners() {
66
- // Remove data-listener-attached attributes so listeners can be re-added
67
- const prevBtn = this.query('.nav-prev')
68
- const nextBtn = this.query('.nav-next')
69
- const closeBtn = this.query('.nav-close')
70
-
71
- if (prevBtn) prevBtn.removeAttribute('data-listener-attached')
72
- if (nextBtn) nextBtn.removeAttribute('data-listener-attached')
73
- if (closeBtn) closeBtn.removeAttribute('data-listener-attached')
74
- }
75
-
76
- // Common render method
77
- protected render() {
78
- // Allow subclasses to opt out of common rendering
79
- if (this.shouldUseCustomRender()) {
80
- this.customRender()
81
- return
82
- }
83
-
84
- const styles = this.css`
85
- :host {
86
- --cc-viewer-z-index-each: 1000;
87
- }
88
-
89
- .backdrop {
90
- justify-content: center;
91
- align-items: center;
92
- position: fixed;
93
- left: 0;
94
- right: 0;
95
- top: 0;
96
- bottom: 0;
97
- width: 100%;
98
- height: 100%;
99
- background-color: rgba(0, 0, 0, 0.9);
100
- z-index: var(--cc-viewer-z-index-each);
101
- }
102
-
103
- .viewer {
104
- position: absolute;
105
- width: 90%;
106
- height: 85%;
107
- inset: 0px;
108
- margin: auto;
109
- align-self: center;
110
- background-color: #000;
111
- }
112
-
113
- ${this.getNavigationStyles()}
114
- ${this.getCustomStyles()}
115
- `
116
-
117
- const html = `
118
- ${styles}
119
- <div class="backdrop" style="${this.isShow ? 'visibility: visible' : 'visibility: hidden'}">
120
- ${this.getNavigationButtons()}
121
- <div class="viewer">
122
- ${this.getViewerContent()}
123
- </div>
124
- </div>
125
- `
126
-
127
- this.updateShadowRoot(html)
128
-
129
- // Add listeners after render
130
- setTimeout(() => {
131
- this.addNavigationListeners()
132
- this.onAfterRender()
133
- }, 0)
134
- }
135
-
136
- // Hook for viewers that need completely custom rendering (e.g., image viewer)
137
- protected shouldUseCustomRender(): boolean {
138
- return false
139
- }
140
-
141
- protected customRender(): void {
142
- // Override in subclasses that need custom rendering
143
- }
144
-
145
- // Hook methods for subclasses
146
- protected getCustomStyles(): string {
147
- return ''
148
- }
149
-
150
- protected onAfterRender(): void {
151
- // Subclasses can override for custom listeners
152
- }
153
-
154
- protected navigatePrev() {
155
- this.dispatch('navigate-prev')
156
- }
157
-
158
- protected navigateNext() {
159
- this.dispatch('navigate-next')
160
- }
161
-
162
- protected getNavigationButtons(): string {
163
- const prevStyle = this.showPrevButton ? '' : 'display: none;'
164
- const nextStyle = this.showNextButton ? '' : 'display: none;'
165
-
166
- return `
167
- <button class="nav-button nav-prev" style="${prevStyle}" aria-label="Previous">
168
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
169
- <polyline points="15 18 9 12 15 6"></polyline>
170
- </svg>
171
- </button>
172
- <button class="nav-button nav-next" style="${nextStyle}" aria-label="Next">
173
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
174
- <polyline points="9 18 15 12 9 6"></polyline>
175
- </svg>
176
- </button>
177
- <button class="nav-button nav-close" aria-label="Close">
178
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
179
- <line x1="18" y1="6" x2="6" y2="18"></line>
180
- <line x1="6" y1="6" x2="18" y2="18"></line>
181
- </svg>
182
- </button>
183
- `
184
- }
185
-
186
- protected getNavigationStyles(): string {
187
- return `
188
- .nav-button {
189
- position: absolute;
190
- top: 50%;
191
- transform: translateY(-50%);
192
- background: rgba(0, 0, 0, 0.5);
193
- color: white;
194
- border: none;
195
- border-radius: 4px;
196
- width: 48px;
197
- height: 48px;
198
- display: flex;
199
- align-items: center;
200
- justify-content: center;
201
- cursor: pointer;
202
- transition: background 0.3s;
203
- z-index: 1002;
204
- pointer-events: auto;
205
- }
206
-
207
- .nav-button:hover {
208
- background: rgba(0, 0, 0, 0.7);
209
- }
210
-
211
- .nav-prev {
212
- left: 20px;
213
- }
214
-
215
- .nav-next {
216
- right: 20px;
217
- }
218
-
219
- .nav-close {
220
- top: 20px;
221
- right: 20px;
222
- transform: none;
223
- }
224
- `
225
- }
226
-
227
- protected addNavigationListeners() {
228
-
229
- // Check if this viewer is actually visible
230
- const backdrop = this.query('.backdrop') as HTMLElement
231
- if (backdrop && backdrop.style.visibility === 'hidden') {
232
- return
233
- }
234
-
235
- setTimeout(() => {
236
- const prevBtn = this.query('.nav-prev')
237
- const nextBtn = this.query('.nav-next')
238
- const closeBtn = this.query('.nav-close')
239
-
240
- if (prevBtn && !prevBtn.hasAttribute('data-listener-attached')) {
241
- prevBtn.setAttribute('data-listener-attached', 'true')
242
- prevBtn.addEventListener('click', (e) => {
243
- e.stopPropagation()
244
- e.preventDefault()
245
- this.navigatePrev()
246
- }, true)
247
- }
248
-
249
- if (nextBtn && !nextBtn.hasAttribute('data-listener-attached')) {
250
- nextBtn.setAttribute('data-listener-attached', 'true')
251
- nextBtn.addEventListener('click', (e) => {
252
- e.stopPropagation()
253
- this.navigateNext()
254
- })
255
- }
256
-
257
- if (closeBtn && !closeBtn.hasAttribute('data-listener-attached')) {
258
- closeBtn.setAttribute('data-listener-attached', 'true')
259
- closeBtn.addEventListener('click', (e) => {
260
- e.stopPropagation()
261
- this.close()
262
- })
263
- }
264
- }, 0)
265
- }
266
-
267
- protected updateNavigationVisibility() {
268
- const prevBtn = this.query('.nav-prev')
269
- const nextBtn = this.query('.nav-next')
270
-
271
- if (prevBtn) {
272
- (prevBtn as HTMLElement).style.display = this._showPrevButton ? '' : 'none'
273
- }
274
-
275
- if (nextBtn) {
276
- (nextBtn as HTMLElement).style.display = this._showNextButton ? '' : 'none'
277
- }
278
- }
1
+ import { ChuciElement } from '@/utils/base-element'
2
+
3
+ export abstract class CcViewerBase extends ChuciElement {
4
+ private _showPrevButton = true
5
+ private _showNextButton = true
6
+ protected isShow = false
7
+ protected isLoading = false
8
+
9
+ get showPrevButton() {
10
+ return this._showPrevButton
11
+ }
12
+
13
+ set showPrevButton(value: boolean) {
14
+ this._showPrevButton = value
15
+ this.updateNavigationVisibility()
16
+ }
17
+
18
+ get showNextButton() {
19
+ return this._showNextButton
20
+ }
21
+
22
+ set showNextButton(value: boolean) {
23
+ this._showNextButton = value
24
+ this.updateNavigationVisibility()
25
+ }
26
+
27
+ // Template method pattern - subclasses implement these
28
+ protected abstract doOpen(url: string): void | Promise<void>
29
+ protected abstract doClose(): void
30
+ protected abstract getViewerContent(): string
31
+
32
+ // Common lifecycle methods
33
+ open(url: string): void {
34
+ this.isShow = true
35
+ this.isLoading = true
36
+
37
+ // Call subclass implementation first
38
+ const openPromise = Promise.resolve(this.doOpen(url))
39
+
40
+ // Use microtask to ensure doOpen runs first (even if synchronous)
41
+ Promise.resolve().then(() => {
42
+ // Initial render for loading state
43
+ this.render()
44
+ })
45
+
46
+ // Update after loading completes
47
+ openPromise.then(() => {
48
+ this.isLoading = false
49
+ this.render()
50
+ }).catch(_error => {
51
+ this.isLoading = false
52
+ this.render()
53
+ })
54
+ }
55
+
56
+ close(): void {
57
+ this.cleanupNavigationListeners()
58
+ this.doClose()
59
+ this.isShow = false
60
+ this.isLoading = false
61
+ this.render()
62
+ this.dispatch('close')
63
+ }
64
+
65
+ protected cleanupNavigationListeners() {
66
+ // Remove data-listener-attached attributes so listeners can be re-added
67
+ const prevBtn = this.query('.nav-prev')
68
+ const nextBtn = this.query('.nav-next')
69
+ const closeBtn = this.query('.nav-close')
70
+
71
+ if (prevBtn) prevBtn.removeAttribute('data-listener-attached')
72
+ if (nextBtn) nextBtn.removeAttribute('data-listener-attached')
73
+ if (closeBtn) closeBtn.removeAttribute('data-listener-attached')
74
+ }
75
+
76
+ // Common render method
77
+ protected render() {
78
+ // Allow subclasses to opt out of common rendering
79
+ if (this.shouldUseCustomRender()) {
80
+ this.customRender()
81
+ return
82
+ }
83
+
84
+ const styles = this.css`
85
+ :host {
86
+ --cc-viewer-z-index-each: 1000;
87
+ }
88
+
89
+ .backdrop {
90
+ justify-content: center;
91
+ align-items: center;
92
+ position: fixed;
93
+ left: 0;
94
+ right: 0;
95
+ top: 0;
96
+ bottom: 0;
97
+ width: 100%;
98
+ height: 100%;
99
+ background-color: rgba(0, 0, 0, 0.9);
100
+ z-index: var(--cc-viewer-z-index-each);
101
+ }
102
+
103
+ .viewer {
104
+ position: absolute;
105
+ width: 90%;
106
+ height: 85%;
107
+ inset: 0px;
108
+ margin: auto;
109
+ align-self: center;
110
+ background-color: #000;
111
+ }
112
+
113
+ ${this.getNavigationStyles()}
114
+ ${this.getCustomStyles()}
115
+ `
116
+
117
+ const html = `
118
+ ${styles}
119
+ <div class="backdrop" style="${this.isShow ? 'visibility: visible' : 'visibility: hidden'}">
120
+ ${this.getNavigationButtons()}
121
+ <div class="viewer">
122
+ ${this.getViewerContent()}
123
+ </div>
124
+ </div>
125
+ `
126
+
127
+ this.updateShadowRoot(html)
128
+
129
+ // Add listeners after render
130
+ setTimeout(() => {
131
+ this.addNavigationListeners()
132
+ this.onAfterRender()
133
+ }, 0)
134
+ }
135
+
136
+ // Hook for viewers that need completely custom rendering (e.g., image viewer)
137
+ protected shouldUseCustomRender(): boolean {
138
+ return false
139
+ }
140
+
141
+ protected customRender(): void {
142
+ // Override in subclasses that need custom rendering
143
+ }
144
+
145
+ // Hook methods for subclasses
146
+ protected getCustomStyles(): string {
147
+ return ''
148
+ }
149
+
150
+ protected onAfterRender(): void {
151
+ // Subclasses can override for custom listeners
152
+ }
153
+
154
+ protected navigatePrev() {
155
+ this.dispatch('navigate-prev')
156
+ }
157
+
158
+ protected navigateNext() {
159
+ this.dispatch('navigate-next')
160
+ }
161
+
162
+ protected getNavigationButtons(): string {
163
+ const prevStyle = this.showPrevButton ? '' : 'display: none;'
164
+ const nextStyle = this.showNextButton ? '' : 'display: none;'
165
+
166
+ return `
167
+ <button class="nav-button nav-prev" style="${prevStyle}" aria-label="Previous">
168
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
169
+ <polyline points="15 18 9 12 15 6"></polyline>
170
+ </svg>
171
+ </button>
172
+ <button class="nav-button nav-next" style="${nextStyle}" aria-label="Next">
173
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
174
+ <polyline points="9 18 15 12 9 6"></polyline>
175
+ </svg>
176
+ </button>
177
+ <button class="nav-button nav-close" aria-label="Close">
178
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
179
+ <line x1="18" y1="6" x2="6" y2="18"></line>
180
+ <line x1="6" y1="6" x2="18" y2="18"></line>
181
+ </svg>
182
+ </button>
183
+ `
184
+ }
185
+
186
+ protected getNavigationStyles(): string {
187
+ return `
188
+ .nav-button {
189
+ position: absolute;
190
+ top: 50%;
191
+ transform: translateY(-50%);
192
+ background: rgba(0, 0, 0, 0.5);
193
+ color: white;
194
+ border: none;
195
+ border-radius: 4px;
196
+ width: 48px;
197
+ height: 48px;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ cursor: pointer;
202
+ transition: background 0.3s;
203
+ z-index: calc(var(--cc-viewer-z-index-each, 1000) + 2);
204
+ pointer-events: auto;
205
+ }
206
+
207
+ .nav-button:hover {
208
+ background: rgba(0, 0, 0, 0.7);
209
+ }
210
+
211
+ .nav-prev {
212
+ left: 20px;
213
+ }
214
+
215
+ .nav-next {
216
+ right: 20px;
217
+ }
218
+
219
+ .nav-close {
220
+ top: 20px;
221
+ right: 20px;
222
+ transform: none;
223
+ }
224
+ `
225
+ }
226
+
227
+ protected addNavigationListeners() {
228
+
229
+ // Check if this viewer is actually visible
230
+ const backdrop = this.query('.backdrop') as HTMLElement
231
+ if (backdrop && backdrop.style.visibility === 'hidden') {
232
+ return
233
+ }
234
+
235
+ setTimeout(() => {
236
+ const prevBtn = this.query('.nav-prev')
237
+ const nextBtn = this.query('.nav-next')
238
+ const closeBtn = this.query('.nav-close')
239
+
240
+ if (prevBtn && !prevBtn.hasAttribute('data-listener-attached')) {
241
+ prevBtn.setAttribute('data-listener-attached', 'true')
242
+ prevBtn.addEventListener('click', (e) => {
243
+ e.stopPropagation()
244
+ e.preventDefault()
245
+ this.navigatePrev()
246
+ }, true)
247
+ }
248
+
249
+ if (nextBtn && !nextBtn.hasAttribute('data-listener-attached')) {
250
+ nextBtn.setAttribute('data-listener-attached', 'true')
251
+ nextBtn.addEventListener('click', (e) => {
252
+ e.stopPropagation()
253
+ this.navigateNext()
254
+ })
255
+ }
256
+
257
+ if (closeBtn && !closeBtn.hasAttribute('data-listener-attached')) {
258
+ closeBtn.setAttribute('data-listener-attached', 'true')
259
+ closeBtn.addEventListener('click', (e) => {
260
+ e.stopPropagation()
261
+ this.close()
262
+ })
263
+ }
264
+ }, 0)
265
+ }
266
+
267
+ protected updateNavigationVisibility() {
268
+ const prevBtn = this.query('.nav-prev')
269
+ const nextBtn = this.query('.nav-next')
270
+
271
+ if (prevBtn) {
272
+ (prevBtn as HTMLElement).style.display = this._showPrevButton ? '' : 'none'
273
+ }
274
+
275
+ if (nextBtn) {
276
+ (nextBtn as HTMLElement).style.display = this._showNextButton ? '' : 'none'
277
+ }
278
+ }
279
279
  }