@electerm/electerm-react 3.7.16 → 3.8.6

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.
@@ -4,13 +4,7 @@
4
4
 
5
5
  import React from 'react'
6
6
  import { Component } from 'manate/react/class-components'
7
- import {
8
- CheckOutlined,
9
- CloseOutlined,
10
- LoadingOutlined
11
- } from '@ant-design/icons'
12
- import createName from '../../common/create-title'
13
- import InputAutoFocus from '../common/input-auto-focus'
7
+ import { LoadingOutlined } from '@ant-design/icons'
14
8
  import { uniq, filter, pick } from 'lodash-es'
15
9
  import {
16
10
  maxBookmarkGroupTitleLength,
@@ -25,11 +19,17 @@ import getInitItem from '../../common/init-setting-item'
25
19
  import uid from '../../common/uid'
26
20
  import { action } from 'manate'
27
21
  import './tree-list.styl'
28
- import TreeExpander from './tree-expander'
29
- import TreeListItem from './tree-list-item'
22
+ import TreeListRow from './tree-list-row'
23
+ import TreeListEditorOverlay from './tree-list-editor-overlay.jsx'
30
24
  import TreeSearch from './tree-search'
31
- import { CategoryColorPicker } from './category-color-picker.jsx'
25
+ import VirtualTreeList from './virtual-tree-list'
26
+ import { buildVisibleTreeRows } from './tree-list-rows'
32
27
  import { getRandomDefaultColor } from '../../common/rand-hex-color.js'
28
+ import {
29
+ treeEditorRowHeight,
30
+ treeLevelIndent,
31
+ treeRowHeight
32
+ } from './tree-list-layout'
33
33
 
34
34
  export default class ItemListTree extends Component {
35
35
  constructor (props) {
@@ -43,9 +43,10 @@ export default class ItemListTree extends Component {
43
43
  bookmarkGroupColor: '',
44
44
  categoryTitle: '',
45
45
  categoryColor: '',
46
- categoryId: ''
46
+ categoryId: '',
47
+ searchSelectedRowKey: ''
47
48
  }
48
- this.treeRef = React.createRef()
49
+ this.listRef = React.createRef()
49
50
  }
50
51
 
51
52
  onSubmit = false
@@ -64,6 +65,13 @@ export default class ItemListTree extends Component {
64
65
  clearTimeout(this.timer)
65
66
  }
66
67
 
68
+ scrollTreeToTop = () => {
69
+ const listWrap = this.listRef.current
70
+ if (listWrap) {
71
+ listWrap.scrollTop = 0
72
+ }
73
+ }
74
+
67
75
  onCancelMoveItem = () => {
68
76
  this.setState({
69
77
  openMoveModal: false,
@@ -72,15 +80,6 @@ export default class ItemListTree extends Component {
72
80
  })
73
81
  }
74
82
 
75
- filter = (list) => {
76
- const { keyword } = this.state
77
- return keyword
78
- ? list.filter(item => {
79
- return createName(item).toLowerCase().includes(keyword.toLowerCase())
80
- })
81
- : list
82
- }
83
-
84
83
  onExpandKey = group => {
85
84
  const {
86
85
  expandedKeys
@@ -104,57 +103,88 @@ export default class ItemListTree extends Component {
104
103
  }
105
104
 
106
105
  handleChange = keyword => {
107
- this.setState({ keyword })
106
+ this.setState({
107
+ keyword,
108
+ searchSelectedRowKey: ''
109
+ })
108
110
  }
109
111
 
110
112
  handleKeyDown = (e) => {
111
113
  const { keyword } = this.state
112
114
  if (!keyword) return
115
+ this.handleVirtualTreeKeyDown(e)
116
+ }
113
117
 
114
- const treeContainer = this.treeRef.current
115
- if (!treeContainer) return
116
-
117
- const allItems = treeContainer.querySelectorAll('.tree-item')
118
- const matchedItems = Array.from(allItems).filter(item => {
119
- const isGroup = item.getAttribute('data-is-group') === 'true'
120
- if (isGroup) return false
121
- const title = item.querySelector('.tree-item-title')
122
- if (!title) return false
123
- const text = title.textContent || ''
124
- return text.toLowerCase().includes(keyword.toLowerCase())
125
- })
126
-
127
- if (matchedItems.length === 0) return
128
-
129
- const currentSelected = treeContainer.querySelector('.tree-item.search-selected')
130
- let currentIndex = -1
131
- if (currentSelected) {
132
- currentSelected.classList.remove('search-selected')
133
- currentIndex = matchedItems.indexOf(currentSelected)
118
+ handleVirtualTreeKeyDown = (e) => {
119
+ const { matchedRowKeys, rows } = this.getVisibleTreeData()
120
+ if (!matchedRowKeys.length) {
121
+ return
134
122
  }
135
123
 
124
+ const { searchSelectedRowKey } = this.state
125
+ const currentIndex = matchedRowKeys.indexOf(searchSelectedRowKey)
126
+
136
127
  if (e.key === 'ArrowDown') {
137
128
  e.preventDefault()
138
- const nextIndex = (currentIndex + 1) % matchedItems.length
139
- matchedItems[nextIndex].classList.add('search-selected')
140
- matchedItems[nextIndex].scrollIntoView({ block: 'nearest' })
141
- } else if (e.key === 'ArrowUp') {
129
+ const nextIndex = (currentIndex + 1) % matchedRowKeys.length
130
+ const rowKey = matchedRowKeys[nextIndex]
131
+ this.setState({ searchSelectedRowKey: rowKey })
132
+ this.scrollRowIntoView(rows, rowKey)
133
+ return
134
+ }
135
+
136
+ if (e.key === 'ArrowUp') {
142
137
  e.preventDefault()
143
- const nextIndex = currentIndex <= 0 ? matchedItems.length - 1 : currentIndex - 1
144
- matchedItems[nextIndex].classList.add('search-selected')
145
- matchedItems[nextIndex].scrollIntoView({ block: 'nearest' })
146
- } else if (e.key === 'Enter') {
138
+ const nextIndex = currentIndex <= 0 ? matchedRowKeys.length - 1 : currentIndex - 1
139
+ const rowKey = matchedRowKeys[nextIndex]
140
+ this.setState({ searchSelectedRowKey: rowKey })
141
+ this.scrollRowIntoView(rows, rowKey)
142
+ return
143
+ }
144
+
145
+ if (e.key === 'Enter') {
147
146
  e.preventDefault()
148
- const target = currentIndex >= 0 ? matchedItems[currentIndex] : matchedItems[0]
149
- if (target) {
150
- const titleEl = target.querySelector('.tree-item-title')
151
- if (titleEl) {
152
- titleEl.click()
153
- }
147
+ const rowKey = currentIndex >= 0
148
+ ? matchedRowKeys[currentIndex]
149
+ : matchedRowKeys[0]
150
+ const row = rows.find(item => item.key === rowKey)
151
+ if (row?.item?.id) {
152
+ this.selectBookmarkById(row.item.id)
154
153
  }
155
154
  }
156
155
  }
157
156
 
157
+ scrollRowIntoView = (rows, rowKey) => {
158
+ const listWrap = this.listRef.current
159
+ if (!listWrap) {
160
+ return
161
+ }
162
+ const rowIndex = rows.findIndex(row => row.key === rowKey)
163
+ if (rowIndex < 0) {
164
+ return
165
+ }
166
+ const rowTop = rowIndex * treeRowHeight
167
+ const rowBottom = rowTop + treeRowHeight
168
+ const viewportTop = listWrap.scrollTop
169
+ const viewportBottom = viewportTop + listWrap.clientHeight
170
+
171
+ if (rowTop < viewportTop) {
172
+ listWrap.scrollTop = rowTop
173
+ } else if (rowBottom > viewportBottom) {
174
+ listWrap.scrollTop = rowBottom - listWrap.clientHeight
175
+ }
176
+ }
177
+
178
+ getVisibleTreeData = () => {
179
+ return buildVisibleTreeRows({
180
+ bookmarkGroups: this.props.bookmarkGroups,
181
+ bookmarkGroupTree: this.props.bookmarkGroupTree,
182
+ bookmarksMap: this.props.bookmarksMap,
183
+ expandedKeys: this.props.expandedKeys,
184
+ keyword: this.state.keyword
185
+ })
186
+ }
187
+
158
188
  handleCancelNew = () => {
159
189
  this.setState({
160
190
  showNewBookmarkGroupForm: false,
@@ -319,7 +349,7 @@ export default class ItemListTree extends Component {
319
349
  bookmarkGroupTitle: '',
320
350
  bookmarkGroupColor: getRandomDefaultColor(),
321
351
  parentId: ''
322
- })
352
+ }, this.scrollTreeToTop)
323
353
  }
324
354
 
325
355
  del = (item, e) => {
@@ -358,16 +388,21 @@ export default class ItemListTree extends Component {
358
388
  : this.onExpandKey
359
389
  func({ id })
360
390
  } else {
361
- store.storeAssign({
362
- currentBookmarkGroupId: findBookmarkGroupId(store.bookmarkGroups, id)
363
- })
364
- const { bookmarks } = this.props
365
- const bookmark = bookmarks.find(
366
- d => d.id === id
367
- )
368
- if (bookmark) {
369
- this.props.onClickItem(bookmark)
370
- }
391
+ this.selectBookmarkById(id)
392
+ }
393
+ }
394
+
395
+ selectBookmarkById = (id) => {
396
+ const { store } = window
397
+ store.storeAssign({
398
+ currentBookmarkGroupId: findBookmarkGroupId(store.bookmarkGroups, id)
399
+ })
400
+ const { bookmarks } = this.props
401
+ const bookmark = bookmarks.find(
402
+ d => d.id === id
403
+ )
404
+ if (bookmark) {
405
+ this.props.onClickItem(bookmark)
371
406
  }
372
407
  }
373
408
 
@@ -610,34 +645,6 @@ export default class ItemListTree extends Component {
610
645
  }
611
646
  })
612
647
 
613
- editCategory = () => {
614
- const {
615
- categoryTitle,
616
- categoryColor
617
- } = this.state
618
- const confirm = (
619
- <span>
620
- <CheckOutlined className='pointer' onClick={this.handleSubmitEdit} />
621
- <CloseOutlined className='mg1l pointer' onClick={this.handleCancelEdit} />
622
- </span>
623
- )
624
- const colorPicker = (
625
- <CategoryColorPicker
626
- value={categoryColor || getRandomDefaultColor()}
627
- onChange={this.handleChangeCategoryColor}
628
- />
629
- )
630
- return (
631
- <InputAutoFocus
632
- value={categoryTitle}
633
- onChange={this.handleChangeEdit}
634
- onPressEnter={this.handleSubmitEdit}
635
- prefix={colorPicker}
636
- suffix={confirm}
637
- />
638
- )
639
- }
640
-
641
648
  duplicateItem = (e, item) => {
642
649
  e.stopPropagation()
643
650
  const { addItem } = window.store
@@ -701,222 +708,148 @@ export default class ItemListTree extends Component {
701
708
  })
702
709
  }
703
710
 
704
- renderItemTitle = (item, isGroup, parentId) => {
705
- if (isGroup && item.id === this.state.categoryId) {
706
- return this.editCategory(item)
707
- }
708
- const itemProps = {
709
- item,
710
- isGroup,
711
- parentId,
712
- leftSidebarWidth: this.props.leftSidebarWidth,
713
- staticList: this.props.staticList,
714
- selectedItemId: this.props.activeItemId,
715
- ...pick(
716
- this,
717
- [
718
- 'del',
719
- 'openAll',
720
- 'openMoveModal',
721
- 'editItem',
722
- 'addSubCat',
723
- 'onSelect',
724
- 'duplicateItem',
725
- 'onDragStart',
726
- 'onDrop',
727
- 'onDragEnter',
728
- 'onDragLeave',
729
- 'onDragOver'
730
- ]
731
- ),
732
- ...pick(
733
- this.state,
734
- [
735
- 'keyword'
736
- ]
737
- )
738
- }
739
- return (
740
- <TreeListItem
741
- {...itemProps}
742
- />
743
- )
744
- }
745
-
746
- handleExport = () => {
747
- document.querySelector('.download-bookmark-icon')?.click()
748
- }
749
-
750
- handleSshConfigs = () => {
751
- window.store.showSshConfigModal = true
752
- }
753
-
754
- renderNewButtons = () => {
711
+ renderVirtualRow = (row, editor) => {
755
712
  return (
756
- <NewButtonsGroup
757
- onNewBookmark={this.handleNewBookmark}
758
- onNewBookmarkGroup={this.handleNewBookmarkGroup}
759
- onExport={this.handleExport}
760
- onSshConfigs={this.handleSshConfigs}
761
- bookmarkGroups={this.props.bookmarkGroups}
762
- bookmarks={this.props.bookmarks}
713
+ <TreeListRow
714
+ row={row}
715
+ keyword={this.state.keyword}
716
+ expandedKeys={this.props.expandedKeys}
717
+ activeItemId={this.props.activeItemId}
718
+ searchSelectedRowKey={this.state.searchSelectedRowKey}
719
+ staticList={this.props.staticList}
720
+ leftSidebarWidth={this.props.leftSidebarWidth}
721
+ {...pick(
722
+ this,
723
+ [
724
+ 'del',
725
+ 'openAll',
726
+ 'openMoveModal',
727
+ 'editItem',
728
+ 'addSubCat',
729
+ 'onSelect',
730
+ 'duplicateItem',
731
+ 'onDragStart',
732
+ 'onDrop',
733
+ 'onDragEnter',
734
+ 'onDragLeave',
735
+ 'onDragOver'
736
+ ]
737
+ )}
738
+ handleExpand={this.onExpandKey}
739
+ handleUnExpand={this.onUnExpandKey}
740
+ isHidden={editor?.hideRowKey === row.key}
763
741
  />
764
742
  )
765
743
  }
766
744
 
767
- renderGroup = (group, index, parentId) => {
768
- const pids = typeof parentId === 'string' ? parentId : ''
769
- const pid = pids + '#' + group.id
770
- const { bookmarkIds = [], bookmarkGroupIds = [] } = group
745
+ getEditorOverlayState = (rows) => {
746
+ const {
747
+ categoryColor,
748
+ categoryId,
749
+ categoryTitle,
750
+ bookmarkGroupColor,
751
+ bookmarkGroupTitle,
752
+ parentId,
753
+ showNewBookmarkGroupForm
754
+ } = this.state
771
755
 
772
- const hasMatchedItems = (ids) => {
773
- const tree = this.props.bookmarksMap
774
- const { keyword } = this.state
775
- return ids.some(id => {
776
- const item = tree.get(id)
777
- return item && createName(item).toLowerCase().includes(keyword.toLowerCase())
778
- })
756
+ if (categoryId) {
757
+ const rowIndex = rows.findIndex(row => row.isGroup && row.item.id === categoryId)
758
+ if (rowIndex < 0) {
759
+ return null
760
+ }
761
+ const row = rows[rowIndex]
762
+ return {
763
+ top: rowIndex * treeRowHeight,
764
+ left: Math.max(0, (row.depth - 1) * treeLevelIndent),
765
+ title: categoryTitle,
766
+ color: categoryColor,
767
+ handleTitleChange: this.handleChangeEdit,
768
+ handleColorChange: this.handleChangeCategoryColor,
769
+ handleSubmit: this.handleSubmitEdit,
770
+ handleCancel: this.handleCancelEdit,
771
+ selectall: true,
772
+ hideRowKey: row.key
773
+ }
779
774
  }
780
775
 
781
- const hasMatchedSubGroup = (bg) => {
782
- const bgIds = bg.bookmarkIds || []
783
- const bgSubIds = bg.bookmarkGroupIds || []
784
- if (hasMatchedItems(bgIds)) return true
785
- return bgSubIds.some(sgid => {
786
- const subBg = window.store.bookmarkGroupTree[sgid]
787
- return subBg && hasMatchedSubGroup(subBg)
788
- })
776
+ if (!showNewBookmarkGroupForm) {
777
+ return null
789
778
  }
790
779
 
791
- if (this.state.keyword) {
792
- if (!hasMatchedItems(bookmarkIds) && !bookmarkGroupIds.some(id => {
793
- const sg = window.store.bookmarkGroupTree[id]
794
- return sg && hasMatchedSubGroup(sg)
795
- })) {
796
- return null
780
+ if (!parentId) {
781
+ return {
782
+ top: 0,
783
+ left: 0,
784
+ title: bookmarkGroupTitle,
785
+ color: bookmarkGroupColor,
786
+ handleTitleChange: this.handleChangeBookmarkGroupTitle,
787
+ handleColorChange: this.handleChangeBookmarkGroupColor,
788
+ handleSubmit: this.handleSubmit,
789
+ handleCancel: this.handleCancelNew,
790
+ insertionGap: {
791
+ index: 0,
792
+ height: treeEditorRowHeight
793
+ }
797
794
  }
798
795
  }
799
- return (
800
- <div key={group.id} className='group-container'>
801
- {
802
- this.renderExpander(group, pid)
803
- }
804
- {
805
- this.renderGroupTitle(group, pids)
806
- }
807
- <div className='group-container-sub'>
808
- {
809
- this.renderNewCat(group, pid)
810
- }
811
- {
812
- this.renderGroupChildren(group, pid)
813
- }
814
- </div>
815
- </div>
816
- )
817
- }
818
796
 
819
- renderNewCat = (group) => {
820
- const {
821
- bookmarkGroupTitle,
822
- bookmarkGroupColor,
823
- parentId,
824
- showNewBookmarkGroupForm
825
- } = this.state
826
- if (!showNewBookmarkGroupForm || group.id !== parentId) {
797
+ const parentIndex = rows.findIndex(row => row.isGroup && row.item.id === parentId)
798
+ if (parentIndex < 0) {
827
799
  return null
828
800
  }
829
- const confirm = (
830
- <span>
831
- <CheckOutlined className='pointer' onClick={this.handleSubmit} />
832
- <CloseOutlined className='mg1l pointer' onClick={this.handleCancelNew} />
833
- </span>
834
- )
835
- const colorPicker = (
836
- <CategoryColorPicker
837
- value={bookmarkGroupColor || getRandomDefaultColor()}
838
- onChange={this.handleChangeBookmarkGroupColor}
839
- />
840
- )
841
- return (
842
- <div className='pd1y'>
843
- <InputAutoFocus
844
- value={bookmarkGroupTitle}
845
- onPressEnter={this.handleSubmit}
846
- onChange={this.handleChangeBookmarkGroupTitle}
847
- prefix={colorPicker}
848
- suffix={confirm}
849
- onBlur={this.handleBlurBookmarkGroupTitle}
850
- />
851
- </div>
852
- )
801
+ const parentRow = rows[parentIndex]
802
+ return {
803
+ top: (parentIndex + 1) * treeRowHeight,
804
+ left: parentRow.depth * treeLevelIndent,
805
+ title: bookmarkGroupTitle,
806
+ color: bookmarkGroupColor,
807
+ handleTitleChange: this.handleChangeBookmarkGroupTitle,
808
+ handleColorChange: this.handleChangeBookmarkGroupColor,
809
+ handleSubmit: this.handleSubmit,
810
+ handleCancel: this.handleCancelNew,
811
+ insertionGap: {
812
+ index: parentIndex + 1,
813
+ height: treeEditorRowHeight
814
+ }
815
+ }
853
816
  }
854
817
 
855
- renderExpander = (group, level) => {
856
- const expProps = {
857
- level,
858
- group,
859
- keyword: this.state.keyword,
860
- expandedKeys: this.props.expandedKeys,
861
- onExpand: this.onExpandKey,
862
- onUnExpand: this.onUnExpandKey
863
- }
818
+ renderVirtualTreeContent = (rows, editor) => {
864
819
  return (
865
- <TreeExpander
866
- {...expProps}
820
+ <VirtualTreeList
821
+ items={rows}
822
+ rowHeight={treeRowHeight}
823
+ containerRef={this.listRef}
824
+ insertionGap={editor?.insertionGap}
825
+ renderItem={row => this.renderVirtualRow(row, editor)}
867
826
  />
868
827
  )
869
828
  }
870
829
 
871
- renderGroupTitle = (group, parentId) => {
872
- return this.renderItemTitle(group, true, parentId)
830
+ renderEditorOverlay = (editor) => {
831
+ return <TreeListEditorOverlay editor={editor} />
873
832
  }
874
833
 
875
- renderGroupChildren = (group, parentId) => {
876
- const {
877
- bookmarkIds = [],
878
- bookmarkGroupIds = [],
879
- id
880
- } = group
881
- const shouldRender = this.state.keyword || this.props.expandedKeys.includes(id)
882
- if (!shouldRender) {
883
- return null
884
- }
885
- const subGroups = this.renderSubGroup(bookmarkGroupIds, parentId)
886
- const childs = this.renderChilds(bookmarkIds, parentId)
887
- if (this.state.keyword && subGroups.length === 0 && childs.length === 0) {
888
- return null
889
- }
890
- return [
891
- ...subGroups,
892
- ...childs
893
- ]
834
+ handleExport = () => {
835
+ document.querySelector('.download-bookmark-icon')?.click()
894
836
  }
895
837
 
896
- renderSubGroup = (bookmarkGroupIds, parentId) => {
897
- const bookmarkGroups = bookmarkGroupIds.map(id => {
898
- return window.store.bookmarkGroupTree[id]
899
- }).filter(d => d)
900
- return bookmarkGroups.map((node, i) => {
901
- return this.renderGroup(node, i, parentId)
902
- })
838
+ handleSshConfigs = () => {
839
+ window.store.showSshConfigModal = true
903
840
  }
904
841
 
905
- renderChilds = (bookmarkIds, pid) => {
906
- const tree = this.props.bookmarksMap
907
- const { keyword } = this.state
908
- const bookmarks = bookmarkIds.map(id => {
909
- const item = tree.get(id)
910
- if (!item) {
911
- return null
912
- }
913
- return createName(item).toLowerCase().includes(keyword.toLowerCase())
914
- ? item
915
- : null
916
- }).filter(d => d)
917
- return bookmarks.map((node) => {
918
- return this.renderItemTitle(node, false, pid)
919
- })
842
+ renderNewButtons = () => {
843
+ return (
844
+ <NewButtonsGroup
845
+ onNewBookmark={this.handleNewBookmark}
846
+ onNewBookmarkGroup={this.handleNewBookmarkGroup}
847
+ onExport={this.handleExport}
848
+ onSshConfigs={this.handleSshConfigs}
849
+ bookmarkGroups={this.props.bookmarkGroups}
850
+ bookmarks={this.props.bookmarks}
851
+ />
852
+ )
920
853
  }
921
854
 
922
855
  render () {
@@ -929,39 +862,14 @@ export default class ItemListTree extends Component {
929
862
  )
930
863
  }
931
864
  const {
932
- bookmarkGroups,
933
865
  type,
934
866
  staticList,
935
867
  listStyle = {}
936
868
  } = this.props
937
- const level1Bookgroups = ready
938
- ? bookmarkGroups.filter(d => {
939
- if (!d.level || d.level < 2) {
940
- if (this.state.keyword) {
941
- const hasMatchedItemsRecursive = (bg) => {
942
- const ids = bg.bookmarkIds || []
943
- const subIds = bg.bookmarkGroupIds || []
944
- const tree = this.props.bookmarksMap
945
- const { keyword } = this.state
946
- const hasMatch = ids.some(id => {
947
- const item = tree.get(id)
948
- return item && createName(item).toLowerCase().includes(keyword.toLowerCase())
949
- })
950
- if (hasMatch) return true
951
- return subIds.some(sgid => {
952
- const subBg = window.store.bookmarkGroupTree[sgid]
953
- return subBg && hasMatchedItemsRecursive(subBg)
954
- })
955
- }
956
- return hasMatchedItemsRecursive(d)
957
- }
958
- return true
959
- }
960
- return false
961
- })
962
- : []
869
+ const { rows } = this.getVisibleTreeData()
870
+ const editor = this.getEditorOverlayState(rows)
963
871
  return (
964
- <div className={`tree-list item-type-${type}`} ref={this.treeRef}>
872
+ <div className={`tree-list item-type-${type}`}>
965
873
  <div className='tree-list-header'>
966
874
  {
967
875
  staticList
@@ -972,9 +880,13 @@ export default class ItemListTree extends Component {
972
880
  this.renderSearch()
973
881
  }
974
882
  </div>
975
- <div className='item-list-wrap' style={listStyle}>
976
- {this.renderNewCat({ id: '' })}
977
- {level1Bookgroups.map(this.renderGroup)}
883
+ <div
884
+ className='item-list-wrap'
885
+ style={listStyle}
886
+ ref={this.listRef}
887
+ >
888
+ {this.renderVirtualTreeContent(rows, editor)}
889
+ {this.renderEditorOverlay(editor)}
978
890
  </div>
979
891
  </div>
980
892
  )