Newer
Older
import {i18n} from './i18n';
import {css, html} from 'lit-element';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import * as commonUtils from '@dbp-toolkit/common/utils';
import * as commonStyles from '@dbp-toolkit/common/styles';
import * as fileHandlingStyles from '@dbp-toolkit/file-handling/src/styles';
import {Icon} from '@dbp-toolkit/common';
import Tabulator from "tabulator-tables";
import {humanFileSize} from "@dbp-toolkit/common/i18next";
import {name as pkgName} from "@dbp-toolkit/file-handling/package.json";
import {send} from "@dbp-toolkit/common/notification";
import {AdapterLitElement} from "@dbp-toolkit/provider/src/adapter-lit-element";
import {classMap} from 'lit-html/directives/class-map.js';
const MODE_TABLE_ONLY = "table-only";
const MODE_FILE_SINK = "file-sink";
const MODE_FILE_SOURCE = "file-source";
export class Clipboard extends ScopedElementsMixin(AdapterLitElement) {
constructor() {
super();
this.lang = 'de';
this.allowedMimeTypes = '*/*';
this.clipboardFiles = {files: ''};
this.clipboardSelectBtnDisabled = true;
this.tabulatorTable = null;
this._onReceiveBeforeUnload = this.onReceiveBeforeUnload.bind(this);
this.filesToSave = [];
this.numberOfSelectedFiles = 0;
this.enabledTargets = 'local';
this.nextcloudWebAppPasswordURL = "";
this.nextcloudWebDavURL = "";
this.nextcloudName = "";
this.nextcloudFileURL = "";
// To avoid a cyclic dependency
import('./file-sink').then(({ FileSink }) => this.defineScopedElement('dbp-file-sink', FileSink));
import('./file-source').then(({ FileSource }) => this.defineScopedElement('dbp-file-source', FileSource));
}
static get scopedElements() {
return {
'dbp-icon': Icon,
};
}
static get properties() {
return {
...super.properties,
lang: { type: String },
allowedMimeTypes: { type: String, attribute: 'allowed-mime-types' },
clipboardSelectBtnDisabled: { type: Boolean, attribute: true },
clipboardFiles: {type: Object, attribute: 'clipboard-files' },
filesToSave: {type: Array, attribute: 'files-to-save' },
numberOfSelectedFiles: {type: Number, attribute: false },
enabledTargets: {type: String, attribute: 'enabled-targets'},
nextcloudWebAppPasswordURL: { type: String, attribute: 'nextcloud-auth-url' },
nextcloudWebDavURL: { type: String, attribute: 'nextcloud-web-dav-url' },
nextcloudName: { type: String, attribute: 'nextcloud-name' },
nextcloudFileURL: { type: String, attribute: 'nextcloud-file-url' },
nextcloudAuthInfo: {type: String, attribute: 'nextcloud-auth-info'},
mode: {type: String, attribute: 'mode'},
allowNesting: {type: Boolean, attribute: 'allow-nesting' },
};
}
_(selector) {
return this.shadowRoot === null ? this.querySelector(selector) : this.shadowRoot.querySelector(selector);
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case "lang":
i18n.changeLanguage(this.lang);
break;
case "clipboardFiles":
this.generateClipboardTable();
break;
}
});
super.update(changedProperties);
}
async firstUpdated() {
// Give the browser a chance to paint
await new Promise((r) => setTimeout(r, 0));
if (this._("#select_all")) {
let boundSelectHandler = this.selectAllFiles.bind(this);
this._("#select_all").addEventListener('click', boundSelectHandler);
}
}
connectedCallback() {
super.connectedCallback();
const that = this;
this.updateComplete.then(() => {
// see: http://tabulator.info/docs/4.7
this.tabulatorTable = new Tabulator(this._("#clipboard-content-table"), {//if you delete the wrapper around the table you need to set a heigh here
maxHeight:"100%",
height:"100%",
layout: "fitColumns",
selectable: true,
selectableRangeMode: "drag",
responsiveLayoutCollapseStartOpen:false,
resizableColumns: false,
placeholder: i18n.t("clipboard.no-data"),
columns: [
{
formatter:"responsiveCollapse", width:32, minWidth:32, align:"center", resizable:false, headerSort:false},
{
title: '<label class="button-container select-all-icon">' +
'<input type="checkbox" id="select_all" name="select_all" value="select_all">' +
'<span class="checkmark" id="select_all_checkmark"></span>' +
'</label>',
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
field: "type",
align: "center",
headerSort: false,
width: 50,
responsive: 1,
formatter: (cell, formatterParams, onRendered) => {
const icon_tag = that.getScopedTagName("dbp-icon");
let icon = `<${icon_tag} name="empty-file" class="nextcloud-picker-icon"></${icon_tag}>`;
return icon;
}
},
{
title: i18n.t("clipboard.file-name"),
responsive: 0,
widthGrow: 5,
minWidth: 150,
field: "name",
sorter: "alphanum",
formatter: (cell) => {
let data = cell.getRow().getData();
if (data.edit) {
cell.getElement().classList.add("fokus-edit");
}
return cell.getValue();
}
},
{
title: i18n.t("clipboard.file-size"),
responsive: 4,
widthGrow: 1,
field: "size",
formatter: (cell, formatterParams, onRendered) => {
return cell.getRow().getData().type === "directory" ? "" : humanFileSize(cell.getValue());
}
},
{
title: i18n.t("clipboard.file-type"),
responsive: 2,
widthGrow: 1,
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
field: "type",
formatter: (cell, formatterParams, onRendered) => {
if (typeof cell.getValue() === 'undefined') {
return "";
}
const [, fileSubType] = cell.getValue().split('/');
return fileSubType;
}
},
{
title: i18n.t("clipboard.file-mod"),
responsive: 3,
widthGrow: 1,
minWidth: 150,
field: "lastModified",
sorter: (a, b, aRow, bRow, column, dir, sorterParams) => {
const a_timestamp = Date.parse(a);
const b_timestamp = Date.parse(b);
return a_timestamp - b_timestamp;
},
formatter: function (cell, formatterParams, onRendered) {
const timestamp = new Date(cell.getValue());
const year = timestamp.getFullYear();
const month = ("0" + (timestamp.getMonth() + 1)).slice(-2);
const date = ("0" + timestamp.getDate()).slice(-2);
const hours = ("0" + timestamp.getHours()).slice(-2);
const minutes = ("0" + timestamp.getMinutes()).slice(-2);
return date + "." + month + "." + year + " " + hours + ":" + minutes;
}
},
{title: "file", field: "file", visible: false}
],
initialSort: [
{column: "name", dir: "asc"},
{column: "type", dir: "asc"},
],
rowClick: (e, row) => {
this.numberOfSelectedFiles = this.tabulatorTable !== null ? this.tabulatorTable.getSelectedRows().length : 0;
if (this.tabulatorTable !== null
&& this.tabulatorTable.getSelectedRows().length === this.tabulatorTable.getRows().filter(row => this.checkFileType(row.getData())).length) {
this._("#select_all").checked = true;
} else {
this._("#select_all").checked = false;
}
},
rowSelectionChanged: (data, rows) => {
if (this.tabulatorTable && this.tabulatorTable.getSelectedRows().length > 0) {
this.clipboardSelectBtnDisabled = false;
} else {
this.clipboardSelectBtnDisabled = true;
}
if (this._("#select_all_checkmark")) {
this._("#select_all_checkmark").title = this.checkAllSelected() ? i18n.t('clipboard.select-nothing') : i18n.t('clipboard.select-all');
}
}
});
that.generateClipboardTable();
});
//Register only one beforeunload Event for the clipboard warning
if (!window.clipboardWarning) {
window.addEventListener('beforeunload', this._onReceiveBeforeUnload, false);
window.clipboardWarning = true;
}
disconnectedCallback() {
//We doesn't want to deregister this event, because we want to use this event over activities
//window.removeEventListener('beforeunload', this._onReceiveBeforeUnload);
super.disconnectedCallback();
}
/**
* Select or deselect all files from tabulator table
*
*/
selectAllFiles() {
let allSelected = this.checkAllSelected();
if (allSelected) {
this.tabulatorTable.getSelectedRows().forEach(row => row.deselect());
this.numberOfSelectedFiles = 0;
} else {
this.tabulatorTable.selectRow(this.tabulatorTable.getRows().filter(row => row.getData().type != 'directory' && this.checkFileType(row.getData(), this.allowedMimeTypes)));
this.numberOfSelectedFiles = this.tabulatorTable.getSelectedRows().length;
/**
* Checks if all files are already selected
* Returns true if all files are selected
*
checkAllSelected() {
if (this.tabulatorTable) {
let maxSelected = this.tabulatorTable.getRows().filter(row => row.getData().type != 'directory' && this.checkFileType(row.getData(), this.allowedMimeTypes)).length;
let selected = this.tabulatorTable.getSelectedRows().length;
if (selected === maxSelected) {
return true;
}
}
return false;
}
/**
* Check mime type of a file, returns true if this.allowedMimeTypes contains the mime type of the file
*
* @param file
checkFileType(file) {
// check if file is allowed
const [fileMainType, fileSubType] = file.type.split('/');
const mimeTypes = this.allowedMimeTypes.split(',');
let deny = true;
mimeTypes.forEach((str) => {
const [mainType, subType] = str.split('/');
deny = deny && ((mainType !== '*' && mainType !== fileMainType) || (subType !== '*' && subType !== fileSubType));
});
if (deny) {
console.log(`mime type ${file.type} of file '${file.name}' is not compatible with ${this.allowedMimeTypes}`);
return false;
}
return true;
}
/**
* If clipboard files and the tabulator table exists, then clear the table and sets the new data
*
*/
generateClipboardTable() {
if (this.clipboardFiles.files) {
let data = [];
for (let i = 0; i < this.clipboardFiles.files.length; i++) {
data[i] = {
name: this.clipboardFiles.files[i].name,
size: this.clipboardFiles.files[i].size,
type: this.clipboardFiles.files[i].type,
lastModified: this.clipboardFiles.files[i].lastModified,
file: this.clipboardFiles.files[i]
};
}
if (this.tabulatorTable !== null) {
this.tabulatorTable.clearData();
this.tabulatorTable.setData(data);
}
}
if (this._("#select_all")) {
this._("#select_all").checked = false;
}
/**
* Sends the files to a provider and throws a notification
*
* @param files
*/
for (let i = 0; i < files.length; i ++) {
await this.sendFileEvent(files[i].file);
}
this.tabulatorTable.deselectRow();
send({
"summary": i18n.t('clipboard.saved-files-title', {count: files.length}),
"body": i18n.t('clipboard.saved-files-body', {count: files.length}),
"type": "success",
"timeout": 5,
});
}
async sendFileEvent(file) {
const data = {"file": file, "data": file};
const event = new CustomEvent("dbp-clipboard-file-picker-file-downloaded",
{ "detail": data, bubbles: true, composed: true });
this.dispatchEvent(event);
}
/**
* Decides if the "beforeunload" event needs to be canceled
*
* @param event
*/
// we don't need to stop if there are no signed files
if (this.clipboardFiles.files.length === 0) {
return;
}
// we need to handle custom events ourselves
if (event.target && event.target.activeElement && event.target.activeElement.nodeName) {
send({
"summary": i18n.t('clipboard.file-warning'),
"body": i18n.t('clipboard.file-warning-body', {count: this.clipboardFiles.files.length}),
"type": "warning",
"timeout": 5,
});
if (!event.isTrusted) {
// note that this only works with custom event since calls of "confirm" are ignored
// in the non-custom event, see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
const result = confirm("##carefulsaveialuge");
// don't stop the page leave if the user wants to leave
if (result) {
return;
}
}
// Cancel the event as stated by the standard
event.preventDefault();
// Chrome requires returnValue to be set
event.returnValue = '';
}
}
/**
* Saves files from an event to the clipboard
*
* @param event
*/
saveFilesToClipboardEvent(event)
{
//save it
let data = {};
let files = [];
if (this.clipboardFiles && this.clipboardFiles.files.length !== 0) {
files = files.concat(this.clipboardFiles.files);
files = files.concat(event.detail.file);
files = files.concat(event.detail.file);
}
this.filesToSave = files;
if (files && files.length !== 0) {
data = {"files": files};
this.sendSetPropertyEvent('clipboard-files', data);
const event = new CustomEvent("dbp-clipboard-file-picker-file-uploaded",
{ bubbles: true, composed: true });
this.dispatchEvent(event);
}
}
/**
* Saves all files from this.filesToSave in clipboard and throws a notification
*
*/
saveFilesToClipboard()
{
//save it
let data = {};
let files = [];
if (this.clipboardFiles && this.clipboardFiles.files.length !== 0) {
files = files.concat(this.clipboardFiles.files);
files = files.concat(this.filesToSave);
} else {
files = files.concat(this.filesToSave);
}
if (this.filesToSave && this.filesToSave.length !== 0) {
data = {"files": files};
this.sendSetPropertyEvent('clipboard-files', data);
const event = new CustomEvent("dbp-clipboard-file-picker-file-uploaded",
{ bubbles: true, composed: true });
this.dispatchEvent(event);
send({
"summary": i18n.t('clipboard.saved-files-title', {count: this.filesToSave.length}),
"body": i18n.t('clipboard.saved-files-body', {count: this.filesToSave.length}),
"type": "success",
"timeout": 5,
});
/**
* Throws a finish notification with the count from the event.detail
*
* @param event
*/
finishedSaveFilesToClipboard(event) {
"summary": i18n.t('clipboard.saved-files-title', {count: event.detail.count}),
"body": i18n.t('clipboard.saved-files-body', {count: event.detail.count}),
"type": "success",
"timeout": 5,
});
}
/**
* Open the file sink with clipboardfiles
*
*/
openFileSink() {
const fileSink = this._("#file-sink-clipboard");
if ( fileSink ) {
this._("#file-sink-clipboard").files = Object.create(this.tabulatorTable.getSelectedData().length > 0 ? this.tabulatorTable.getSelectedData() : this.clipboardFiles.files);
this._("#file-sink-clipboard").openDialog();
}
}
/**
* Open the file source with clipboardfiles
*
openFileSource() {
const fileSource = this._("#file-source-clipboard");
if (fileSource) {
this._("#file-source-clipboard").openDialog();
/**
* Delete all or only selected files from clipboard and throws a notification
*
*/
if (this.tabulatorTable && this.tabulatorTable.getSelectedData().length > 0) {
let count = this.tabulatorTable.getSelectedData().length;
this.tabulatorTable.deleteRow(this.tabulatorTable.getSelectedRows());
let data = {"files": []};
this.tabulatorTable.getRows().forEach(row =>
data.files.push( row.getData().file )
);
this.sendSetPropertyEvent('clipboard-files', data);
send({
"summary": i18n.t('clipboard.clear-count-clipboard-title', {count: count}),
"body": i18n.t('clipboard.clear-count-clipboard-body', {count: count}),
"type": "success",
"timeout": 5,
});
this.numberOfSelectedFiles = 0;
} else {
let data = {"files": []};
this.sendSetPropertyEvent('clipboard-files', data);
send({
"summary": i18n.t('clipboard.clear-clipboard-title'),
"body": i18n.t('clipboard.clear-clipboard-body'),
"type": "success",
"timeout": 5,
});
}
}
/**
* Get the additional clipboard buttons
* If this.mode === MODE_FILE_SINK or MODE_FILE_SOURCE then there are only delete and save files buttons available
* Else there are the add, delete and save files buttons available
*
let buttonsAreDisabled = this.clipboardFiles.files.length === 0 ? true : this.clipboardSelectBtnDisabled;
<div class="flex-container additional-button-container">
<div class="btn-flex-container-mobile">
<button @click="${() => { this.openFileSource(); }}"
class="button ${classMap({hidden: this.mode === MODE_FILE_SINK || this.mode === MODE_FILE_SOURCE})}" title="${i18n.t('clipboard.add-files')}">
<dbp-icon class="nav-icon" name="clipboard"></dbp-icon> ${i18n.t('clipboard.add-files-btn')}
</button>
<button @click="${() => { this.clearClipboard(); }}"
class="button" title="${(this.numberOfSelectedFiles > 0) ? i18n.t('clipboard.remove-count', {count: this.numberOfSelectedFiles}) : i18n.t('clipboard.remove-all')}"
${(this.numberOfSelectedFiles > 0) ? i18n.t('clipboard.remove-count-btn', {count: this.numberOfSelectedFiles}) : i18n.t('clipboard.remove-all-btn')}
</button>
</div>
<div class="btn-flex-container-mobile">
<button @click="${() => { this.openFileSink(); }}"
class="button" title="${(this.numberOfSelectedFiles > 0) ? i18n.t('clipboard.save-count', {count: this.numberOfSelectedFiles}) : i18n.t('clipboard.save-all')}">
${(this.numberOfSelectedFiles > 0) ? i18n.t('clipboard.save-count-btn', {count: this.numberOfSelectedFiles}) : i18n.t('clipboard.save-all-btn')}
</button>
</div>
</div>
<dbp-file-source
context="${i18n.t('clipboard.add-files')}"
allowed-mime-types="${this.allowedMimeTypes}"
nextcloud-auth-url="${this.nextcloudWebAppPasswordURL}"
nextcloud-web-dav-url="${this.nextcloudWebDavURL}"
nextcloud-name="${this.nextcloudName}"
nextcloud-file-url="${this.nextcloudFileURL}"
nexcloud-auth-info="${this.nextcloudAuthInfo}"
enabled-targets="${this.allowNesting ? this.enabledTargets : this.enabledTargets.replace('clipboard', '')}"
decompress-zip
lang="${this.lang}"
text="${i18n.t('clipboard.upload-area-text')}"
button-label="${i18n.t('clipboard.upload-button-label')}"
show-clipboard
@dbp-file-source-file-selected="${this.saveFilesToClipboardEvent}"
@dbp-nextcloud-file-picker-number-files="${this.finishedSaveFilesToClipboard}"
@dbp-file-source-file-upload-finished="${this.finishedSaveFilesToClipboard}"
<dbp-file-sink id="file-sink-clipboard"
context="${(this.numberOfSelectedFiles > 0) ? i18n.t('clipboard.save-count', {count: this.numberOfSelectedFiles}) : i18n.t('clipboard.save-all')}"
filename="clipboard-documents.zip"
allowed-mime-types="${this.allowedMimeTypes}"
enabled-targets="${this.allowNesting ? this.enabledTargets : this.enabledTargets.replace('clipboard', '')}"
show-clipboard
nextcloud-auth-url="${this.nextcloudWebAppPasswordURL}"
nextcloud-web-dav-url="${this.nextcloudWebDavURL}"
nextcloud-name="${this.nextcloudName}"
nextcloud-file-url="${this.nextcloudFileURL}"
nexcloud-auth-info="${this.nextcloudAuthInfo}"
lang="${this.lang}"
></dbp-file-sink>
`;
}
/**
* Get the clipboard sink html
*
const tabulatorCss = commonUtils.getAssetURL(pkgName, 'tabulator-tables/css/tabulator.min.css');
<div class="wrapper">
<div class="content">
<h3>${i18n.t('clipboard.sink-title')}</h3>
<div class="warning-container">
<dbp-icon name="warning-high" class="warning-icon"></dbp-icon>
<p>${i18n.t('clipboard.warning')}</p>
</div>
<link rel="stylesheet" href="${tabulatorCss}">
<div class="table-wrapper">
<table id="clipboard-content-table" class="force-no-select"></table>
</div>
</div>
</div>
<div class="clipboard-footer">
<button class="button select-button is-primary" title="${i18n.t('clipboard.sink-btn', {count: this.filesToSave.length})}"
@click="${() => {this.saveFilesToClipboard();}}"> <dbp-icon class="nav-icon" name="clipboard"></dbp-icon> ${i18n.t('clipboard.sink-btn', {count: this.filesToSave.length})}
</button>
</div>
/**
* Get the clipboard source html
*
const tabulatorCss = commonUtils.getAssetURL(pkgName, 'tabulator-tables/css/tabulator.min.css');
<div class="wrapper">
<div class="content">
<h3>${i18n.t('clipboard.source-title')}</h3>
<div class="warning-container">
<dbp-icon name="warning-high" class="warning-icon"></dbp-icon>
<p>${i18n.t('clipboard.warning')}</p>
</div>
<link rel="stylesheet" href="${tabulatorCss}">
<div class="table-wrapper">
<table id="clipboard-content-table" class="force-no-select"></table>
</div>
</div>
</div>
<div class="clipboard-footer">
<button class="button select-button is-primary" ?disabled="${this.clipboardSelectBtnDisabled}"
@click="${() => {this.sendClipboardFiles(this.tabulatorTable.getSelectedData());}}"> ${this.tabulatorTable && this.tabulatorTable.getSelectedRows().length > 0 ? i18n.t('clipboard.source-btn', {count: this.tabulatorTable ? this.tabulatorTable.getSelectedRows().length : 0}) : i18n.t('clipboard.source-btn-none')}
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
</div>
`;
}
static get styles() {
// language=css
return css`
${commonStyles.getThemeCSS()}
${commonStyles.getGeneralCSS(false)}
${commonStyles.getButtonCSS()}
${commonStyles.getTextUtilities()}
${commonStyles.getModalDialogCSS()}
${commonStyles.getRadioAndCheckboxCss()}
${fileHandlingStyles.getFileHandlingCss()}
a {
border-bottom: 1px solid rgba(0,0,0,0.3);
padding: 0;
}
a:hover {
color: #fff;
background-color: #000;
}
h2:first-child {
margin-top: 0;
margin-bottom: 0px;
}
.subheadline{
font-style: italic;
padding-left: 2em;
margin-top: -1px;
margin-bottom: 1.2em;
}
.warning-container{
display: flex;
flex-direction: inherit;
align-items: center;
}
.warning-icon{
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
}
.container{
margin-top: 2rem;
}
.flex-container{
margin-bottom: 5px;
}
.select-btn-wrapper{
float: right;
}
.init{
margin: 0px;
}
.flex-container{
display: flex;
justify-content: space-between;
}
.tabulator .tabulator-tableHolder .tabulator-placeholder span{
margin: initial;
}
.checkmark{
height: 20px;
width:20px;
}
.button-container .checkmark::after{
left: 8px;
top: 3px;
width: 4px;
height: 11px;
}
.table-wrapper{
position: relative;
}
.select-all-icon{
background-color: white;
width: 100%;
padding-top: 10px;
display: flex;
align-items: end;
flex-direction: column;
}
.wrapper{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.content{
width: 100%;
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
.additional-button-container{
margin-top: 0.5rem;
}
.warning-container p{
margin-top: 0px;
}
@media only screen
and (orientation: portrait)
.flex-container{
justify-content: space-between;
display: flex;
}
.btn-flex-container-mobile{
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.select-btn-wrapper{
width: 100%;
display: flex;
justify-content: end;
float: none;
}
.flex-container{
display: block;
}
.checkmark{
}
.button-container .checkmark::after{
width: 8px;
height: 15px;
}
.select-all-icon{
}
.btn-flex-container-mobile{
flex-direction: column;
}
.btn-flex-container-mobile button:nth-child(2){
margin-top: 5px;
}
.warning-icon{
margin-right: 10px;
font-size: 85px;
margin-top: -43px;
}
}
`;
}
render() {
const tabulatorCss = commonUtils.getAssetURL(pkgName, 'tabulator-tables/css/tabulator.min.css');
if (this.mode === MODE_FILE_SINK) {
else if (this.mode === MODE_FILE_SOURCE) {
} else {
return html`
<div>
${this.getAdditionalButtons()}
<link rel="stylesheet" href="${tabulatorCss}">
<div class="table-wrapper">
<table id="clipboard-content-table" class="force-no-select"></table>
</div>
</div>
` ;