mirror of
https://github.com/Elvanos/fantasia-archive.git
synced 2024-06-02 02:14:47 +12:00
1108 lines
35 KiB
Vue
1108 lines
35 KiB
Vue
<template>
|
|
|
|
<q-dialog
|
|
v-model="dialogModel"
|
|
@before-hide="triggerDialogClose"
|
|
:persistent="exportOngoing"
|
|
|
|
>
|
|
<q-card
|
|
v-if="!exportOngoing"
|
|
class="exportDialog"
|
|
dark
|
|
>
|
|
<q-card-section class="row justify-center text-center">
|
|
<h6 class="text-center q-my-sm">Export project/documents</h6>
|
|
</q-card-section>
|
|
|
|
<q-card-section>
|
|
<div class="row justify-center">
|
|
<div class="col-4">
|
|
<div class="q-mx-lg">
|
|
<q-select
|
|
class="exportTypeSelect"
|
|
dark
|
|
popup-content-class="menuResizer"
|
|
:options="exportFormats"
|
|
label="Export file format"
|
|
filled
|
|
input-debounce="0"
|
|
v-model="selectedExportFormat"
|
|
/>
|
|
|
|
<q-checkbox
|
|
class="q-mt-lg"
|
|
dark color="primary"
|
|
v-model="exportWholeProject"
|
|
label="Export whole project?"
|
|
/>
|
|
|
|
<q-checkbox
|
|
class="q-mt-lg"
|
|
dark color="primary"
|
|
v-model="includeTags"
|
|
label="Include tags in the export?"
|
|
/>
|
|
|
|
<q-checkbox
|
|
class="q-mt-lg"
|
|
dark color="primary"
|
|
v-model="includeHierarchyPath"
|
|
label="Include hierarchical path in the export?"
|
|
/>
|
|
|
|
<q-checkbox
|
|
class="q-mt-lg"
|
|
dark color="primary"
|
|
v-model="includeIsDead"
|
|
label="Include dead/gone/destroyed documents in the export?"
|
|
/>
|
|
|
|
<q-checkbox
|
|
v-if="includeIsDead"
|
|
class="q-mt-lg"
|
|
dark color="primary"
|
|
v-model="hideDeadInformation"
|
|
label="Hide dead/gone/destroyed status in the exported documents?"
|
|
/>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-8">
|
|
<div
|
|
style="height: 100%;"
|
|
class="q-mx-lg"
|
|
>
|
|
<div
|
|
style="height: 100%; line-height: 2;"
|
|
class="column justify-center items-center"
|
|
v-if="exportWholeProject"
|
|
>
|
|
Please note that:
|
|
<span class="text-bold text-secondary">
|
|
The more documents, the slower export.
|
|
</span>
|
|
<span>
|
|
FA currently needs to generate individual files for <span class="text-bold text-primary">{{SGET_allDocuments.docs.length}} </span> documents.
|
|
</span>
|
|
</div>
|
|
<q-select
|
|
ref="ref_exportDocument"
|
|
class="exportDocumentSelect"
|
|
dark
|
|
popup-content-class="menuResizer"
|
|
v-if="!exportWholeProject"
|
|
menu-anchor="bottom middle"
|
|
menu-self="top middle"
|
|
:options="filteredExistingInput"
|
|
use-input
|
|
multiple
|
|
use-chips
|
|
filled
|
|
label="Selected documents"
|
|
input-debounce="500"
|
|
v-model="exportDocumentsModel"
|
|
@filter="filterExistingSelect"
|
|
>
|
|
<template v-slot:append v-if="!hideAdvSearchCheatsheetButton">
|
|
<q-btn round dense flat icon="mdi-help-rhombus" @click.stop.prevent="SSET_setAdvSearchWindowVisible"
|
|
>
|
|
<q-tooltip :delay="500">
|
|
Open search cheatsheet
|
|
</q-tooltip>
|
|
</q-btn>
|
|
</template>
|
|
<template v-slot:option="{ itemProps, itemEvents, opt }">
|
|
<q-item
|
|
:class="{'hasTextShadow': textShadow, 'isMinor':opt.isMinor}"
|
|
v-bind="itemProps"
|
|
v-on="itemEvents"
|
|
:key="opt.id"
|
|
:style="`color: ${opt.color}; background-color: ${opt.bgColor}`"
|
|
@mouseleave="setDocumentPreviewClose"
|
|
>
|
|
<documentPreview
|
|
:document-id="opt._id"
|
|
:external-close-trigger="documentPreviewClose"
|
|
:special-z-index="999999999"
|
|
:custom-anchor="'top end'"
|
|
:custom-self="'center left'"
|
|
:custom-delay="1200"
|
|
/>
|
|
<q-item-section avatar>
|
|
<q-icon
|
|
:style="`color: ${retrieveIconColor(opt)}`"
|
|
:name="(opt.isCategory) ? 'fas fa-folder-open' : opt.icon"
|
|
/>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label>
|
|
<span class="isDeadIndicator" v-if="opt.isDead">
|
|
†
|
|
</span>
|
|
<span :class="{'isDead': (opt.isDead && !hideDeadCrossThrough)}" v-html="opt.label">
|
|
</span>
|
|
</q-item-label>
|
|
<q-item-label caption class="text-cultured" v-html="opt.hierarchicalPath"></q-item-label>
|
|
<q-item-label caption class="text-cultured" v-if="opt.tags">
|
|
<q-chip
|
|
v-for="(input,index) in opt.tags" :key="index"
|
|
outline
|
|
style="opacity: 0.8;"
|
|
size="12px"
|
|
class="text-cultured"
|
|
v-html="`${input}`"
|
|
>
|
|
</q-chip>
|
|
</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
</template>
|
|
<template v-slot:selected-item="scope">
|
|
<q-chip
|
|
removable
|
|
dense
|
|
@remove="removeInput(scope)"
|
|
:tabindex="scope.tabindex"
|
|
:color="(scope.opt.isAutoGenerated) ? 'teal-3' : 'accent'"
|
|
text-color="dark"
|
|
class="text-bold"
|
|
>
|
|
|
|
<div
|
|
class="relationShipChipOverlay"
|
|
@mouseleave="setDocumentPreviewClose"
|
|
/>
|
|
|
|
<div class="relationShipChipContent">
|
|
<template v-if="scope.opt.isDead">
|
|
†
|
|
</template>
|
|
{{ stripTags(scope.opt.label) }}
|
|
</div>
|
|
<documentPreview
|
|
:special-z-index="999999999"
|
|
:custom-delay="1200"
|
|
:document-id="scope.opt._id"
|
|
:external-close-trigger="documentPreviewClose"
|
|
/>
|
|
|
|
</q-chip>
|
|
</template>
|
|
</q-select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</q-card-section>
|
|
|
|
<q-card-actions align="right" class="q-mb-lg q-mr-xl">
|
|
<q-btn flat label="Cancel" color="accent" v-close-popup class="q-mr-lg" />
|
|
<q-btn
|
|
:flat="!exportWholeProject && exportDocumentsModel.length === 0"
|
|
:outline="exportWholeProject || exportDocumentsModel.length > 0"
|
|
label="Export"
|
|
color="primary"
|
|
:disable="!exportWholeProject && exportDocumentsModel.length === 0"
|
|
@click="exportDocuments"
|
|
/>
|
|
</q-card-actions>
|
|
</q-card>
|
|
|
|
<q-card v-if="exportOngoing" dark class="exportDialog">
|
|
<q-card-section class="row justify-center">
|
|
<h6 class="text-center q-my-sm">Exporting...</h6>
|
|
</q-card-section>
|
|
|
|
<q-card-section class="row justify-center q-mx-xl">
|
|
<div>
|
|
Current document: {{currentDocName}}
|
|
</div>
|
|
</q-card-section>
|
|
|
|
<q-card-section class="row justify-center q-mx-xl q-mb-lg">
|
|
<q-linear-progress stripe round dark size="20px" :value="progressCounter" color="primary" class="q-mt-sm">
|
|
<div class="absolute-full flex flex-center">
|
|
<q-badge text-color="accent" color="dark" :label="`${exportedDocuments}/${exportList.length}`" />
|
|
</div>
|
|
</q-linear-progress>
|
|
</q-card-section>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
|
|
import { Component, Watch, Prop } from "vue-property-decorator"
|
|
import { remote } from "electron"
|
|
import { retrieveCurrentProjectName } from "src/scripts/projectManagement/projectManagent"
|
|
// @ts-ignore
|
|
import json2md from "json2md/lib/index.js"
|
|
// @ts-ignore
|
|
import PDFkit from "pdfkit/js/pdfkit.standalone.js"
|
|
// @ts-ignore
|
|
import htmlParseStringify from "html-parse-stringify/dist/html-parse-stringify.modern.js"
|
|
import DialogBase from "src/components/dialogs/_DialogBase"
|
|
import { uid, extend } from "quasar"
|
|
import fs from "fs-extra"
|
|
import documentPreview from "src/components/DocumentPreview.vue"
|
|
|
|
import { I_ExportObject } from "src/interfaces/I_ExportObject"
|
|
import { I_ShortenedDocument } from "src/interfaces/I_OpenedDocument"
|
|
import { I_Blueprint } from "src/interfaces/I_Blueprint"
|
|
import { I_PDFKitDocument } from "src/interfaces/I_PDFKitDocument"
|
|
import { I_HtmlParserNode } from "src/interfaces/I_HtmlParserNode"
|
|
import { advancedDocumentFilter } from "src/scripts/utilities/advancedDocumentFilter"
|
|
|
|
@Component({
|
|
components: {
|
|
documentPreview
|
|
}
|
|
})
|
|
export default class ExportProject extends DialogBase {
|
|
/**
|
|
* React to dialog opening request
|
|
*/
|
|
@Watch("dialogTrigger")
|
|
openDialog (val: string|false) {
|
|
if (val) {
|
|
if (this.SGET_getDialogsState) {
|
|
return
|
|
}
|
|
this.SSET_setDialogState(true)
|
|
this.dialogModel = true
|
|
|
|
this.resetLocalData()
|
|
this.reloadOptions()
|
|
this.populateExportObjectDialog()
|
|
|
|
if (this.prepickedIds.length > 0) {
|
|
// @ts-ignore
|
|
this.exportDocumentsModel = this.SGET_allDocuments.docs.filter(doc => {
|
|
return this.prepickedIds.includes(doc._id)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
resetLocalData () {
|
|
this.selectedExportFormat = "Adobe Reader - PDF"
|
|
this.exportWholeProject = false
|
|
this.includeTags = false
|
|
this.includeHierarchyPath = false
|
|
this.hideDeadInformation = false
|
|
this.includeIsDead = true
|
|
this.exportDocumentsModel = []
|
|
this.exportOngoing = false
|
|
this.exportList = []
|
|
}
|
|
|
|
@Prop(({
|
|
default () {
|
|
return []
|
|
}
|
|
})) readonly prepickedIds!: string[]
|
|
|
|
exportFormats = [
|
|
"Adobe Reader - PDF",
|
|
"Markdown - MD"
|
|
// "Open office document - ODT",
|
|
// "MS Word document - DOCX"
|
|
]
|
|
|
|
selectedExportFormat = "Adobe Reader - PDF"
|
|
|
|
exportWholeProject = false
|
|
|
|
includeTags = false
|
|
|
|
includeHierarchyPath = false
|
|
|
|
hideDeadInformation = false
|
|
|
|
includeIsDead = true
|
|
|
|
setDocumentPreviewClose () {
|
|
this.documentPreviewClose = uid()
|
|
}
|
|
|
|
/**
|
|
* Reloads local options
|
|
*/
|
|
reloadOptions () {
|
|
this.textShadow = this.SGET_options.textShadow
|
|
this.hideDeadCrossThrough = this.SGET_options.hideDeadCrossThrough
|
|
this.hideAdvSearchCheatsheetButton = this.SGET_options.hideAdvSearchCheatsheetButton
|
|
}
|
|
|
|
/**
|
|
* Hides the advanced search cheatsheet help button in relationship type fields.
|
|
*/
|
|
hideAdvSearchCheatsheetButton = false
|
|
|
|
/**
|
|
* Determines if the "dead" document type should have a cross-text decoration or not
|
|
*/
|
|
hideDeadCrossThrough = false
|
|
|
|
/**
|
|
* Determines if text shadow will be shows for accesiblity reasons or not
|
|
*/
|
|
textShadow = false
|
|
|
|
documentPreviewClose = ""
|
|
|
|
/**
|
|
* Currently being opened document
|
|
*/
|
|
exportDocumentsModel = []
|
|
|
|
/**
|
|
* Pre-filtered list based on the category inclussion or exlcussion
|
|
*/
|
|
existingObjectsFullList = [] as I_ShortenedDocument[]
|
|
|
|
/**
|
|
* All currently loaded blueprints
|
|
*/
|
|
allDocumentBluePrints = [] as I_Blueprint[]
|
|
|
|
/**
|
|
* Filtered list of items
|
|
*/
|
|
filteredExistingInput = null as unknown as I_ShortenedDocument[]
|
|
|
|
/**
|
|
* Local list copty for filtering in order to not mess up the original list
|
|
*/
|
|
listCopy: I_ShortenedDocument[] = []
|
|
|
|
/**
|
|
* Refocuses the first value in the selct upon filtering for intuitive keyboard control
|
|
*/
|
|
async refocusSelect () {
|
|
await this.$nextTick()
|
|
/*eslint-disable */
|
|
// @ts-ignore
|
|
this.$refs.ref_exportDocument.setOptionIndex(-1)
|
|
// @ts-ignore
|
|
this.$refs.ref_exportDocument.moveOptionSelection(1, true)
|
|
/* eslint-enable */
|
|
}
|
|
|
|
/**
|
|
* Filter the pre-filtered list
|
|
*/
|
|
filterExistingSelect (val: string, update: (e: () => void) => void) {
|
|
if (val === "") {
|
|
update(() => {
|
|
this.filteredExistingInput = this.existingObjectsFullList.filter((obj) => !obj.isMinor)
|
|
if (this.$refs.ref_existingDocument && this.filteredExistingInput.length > 0) {
|
|
this.refocusSelect().catch(e => console.log(e))
|
|
}
|
|
})
|
|
return
|
|
}
|
|
|
|
update(() => {
|
|
const needle = val.toLowerCase()
|
|
this.listCopy = extend(true, [], this.existingObjectsFullList)
|
|
this.filteredExistingInput = advancedDocumentFilter(needle, this.listCopy, this.allDocumentBluePrints, this.existingObjectsFullList)
|
|
|
|
if (this.$refs.ref_existingDocument && this.filteredExistingInput.length > 0) {
|
|
this.refocusSelect().catch(e => console.log(e))
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Set up up all data in to the dialog on popup load
|
|
*/
|
|
populateExportObjectDialog () {
|
|
this.allDocumentBluePrints = this.SGET_allBlueprints
|
|
|
|
this.existingObjectsFullList = this.SGET_allDocuments.docs
|
|
}
|
|
|
|
async removeInput (scope: {
|
|
index: number
|
|
removeAtIndex: (index: number) => void
|
|
}) {
|
|
scope.removeAtIndex(scope.index)
|
|
|
|
await this.$nextTick()
|
|
/*eslint-disable */
|
|
// @ts-ignore
|
|
this.$refs.ref_exportDocument.hidePopup()
|
|
/* eslint-enable */
|
|
}
|
|
|
|
exportOngoing = false
|
|
|
|
exportList:I_ShortenedDocument[] = []
|
|
|
|
exportedDocuments = 0
|
|
|
|
currentDocName = ""
|
|
|
|
get progressCounter () {
|
|
return (this.exportedDocuments / this.exportList.length)
|
|
}
|
|
|
|
exportDocuments () {
|
|
remote.dialog.showOpenDialog({
|
|
properties: ["openDirectory"]
|
|
}).then(async (result) => {
|
|
const folderPath = result.filePaths[0]
|
|
|
|
if (!folderPath) {
|
|
return
|
|
}
|
|
|
|
const projectName: string = await retrieveCurrentProjectName()
|
|
|
|
const exportPath = `${folderPath}/${projectName} - Export`
|
|
|
|
if (!fs.existsSync(exportPath)) {
|
|
fs.mkdirSync(exportPath)
|
|
}
|
|
|
|
this.exportOngoing = true
|
|
this.exportedDocuments = 0
|
|
this.exportList = (this.exportWholeProject) ? this.SGET_allDocuments.docs : this.exportDocumentsModel
|
|
|
|
if (!this.includeIsDead) {
|
|
this.exportList = this.exportList.filter(doc => {
|
|
return doc.extraFields.find(e => e.id === "deadSwitch")?.value !== true
|
|
})
|
|
}
|
|
|
|
// @ts-ignore
|
|
this.exportList = this.exportList.map(doc => {
|
|
return this.SGET_allDocuments.docs.find(subDoc => subDoc._id === doc._id)
|
|
})
|
|
|
|
for (const document of this.exportList) {
|
|
this.currentDocName = document.label
|
|
|
|
// Build the export data object
|
|
const exportObject = this.buildExportObject(document)
|
|
|
|
// MD export
|
|
if (this.selectedExportFormat === "Markdown - MD") {
|
|
this.exportFile_MD(exportObject, exportPath)
|
|
}
|
|
|
|
// PDF
|
|
if (this.selectedExportFormat === "Adobe Reader - PDF") {
|
|
this.exportFile_PDF(exportObject, exportPath)
|
|
}
|
|
|
|
await this.sleep(10)
|
|
this.exportedDocuments++
|
|
}
|
|
|
|
// Cleanup
|
|
this.exportOngoing = false
|
|
this.$q.notify({
|
|
group: false,
|
|
type: "positive",
|
|
message: "Export finished"
|
|
})
|
|
}).catch(err => {
|
|
console.log(err)
|
|
})
|
|
}
|
|
|
|
buildExportObject (input: I_ShortenedDocument): I_ExportObject {
|
|
const matchingBlueprint = this.SGET_blueprint(input.type)
|
|
|
|
const exportObject = {
|
|
name: input.extraFields.find(e => e.id === "name")?.value,
|
|
documentType: matchingBlueprint.nameSingular,
|
|
documentDirectory: matchingBlueprint.namePlural,
|
|
isCategory: input.extraFields.find(e => e.id === "categorySwitch")?.value,
|
|
fieldValues: this.buildFieldValues(input, matchingBlueprint)
|
|
} as I_ExportObject
|
|
|
|
if (!this.hideDeadInformation && this.includeIsDead) {
|
|
exportObject.isDead = input.extraFields.find(e => e.id === "deadSwitch")?.value
|
|
}
|
|
else {
|
|
exportObject.isDead = false
|
|
}
|
|
|
|
if (this.includeTags) {
|
|
exportObject.tags = input.extraFields.find(e => e.id === "tags")?.value
|
|
}
|
|
if (this.includeHierarchyPath) {
|
|
// @ts-ignore
|
|
exportObject.hierarchicalPath = this.getDocumentHieararchicalPath(input, this.SGET_allDocuments.docs)
|
|
}
|
|
|
|
return exportObject
|
|
}
|
|
|
|
buildFieldValues (input: I_ShortenedDocument, blueprint: I_Blueprint) {
|
|
const catIgnoreList = ["breakDocumentSettings", "name", "documentColor", "documentBackgroundColor", "parentDoc", "order", "categorySwitch", "minorSwitch", "deadSwitch", "finishedSwitch", "tags", "otherNames", "categoryDescription"]
|
|
|
|
// Filter and map all fields
|
|
const mappedFields = blueprint.extraFields
|
|
.filter(field => field.type !== "tags")
|
|
.filter(field => field.type !== "switch")
|
|
.filter(field => field.id !== "name")
|
|
.filter(field => field.id !== "order")
|
|
.filter(field => field.id !== "deadSwitch")
|
|
.filter(field => field.id !== "categorySwitch")
|
|
.filter(field => field.id !== "parentDoc")
|
|
.filter(field => field.id !== "documentColor")
|
|
.filter(field => field.id !== "documentBackgroundColor")
|
|
.filter(field => field.id !== "breakDocumentSettings")
|
|
.filter(field => !field.isLegacy)
|
|
.filter(field => {
|
|
if (input.extraFields.find(e => e.id === "categorySwitch")?.value) {
|
|
if (catIgnoreList.includes(field.id)) {
|
|
return true
|
|
}
|
|
else {
|
|
return false
|
|
}
|
|
}
|
|
else {
|
|
return true
|
|
}
|
|
})
|
|
.map(field => {
|
|
const matchedField = input.extraFields.find(sub => sub.id === field.id)
|
|
let returnValue = matchedField?.value
|
|
|
|
// Convert numbers to strings
|
|
if (field.type === "number" && typeof returnValue === "number") {
|
|
returnValue = returnValue.toString()
|
|
}
|
|
|
|
// Build string out of lists
|
|
if (field.type === "list" && Array.isArray(returnValue)) {
|
|
if (!field.predefinedListExtras) {
|
|
returnValue = returnValue.map((e: {value: string}) => `${e.value}`)
|
|
}
|
|
else {
|
|
returnValue = (field.predefinedListExtras?.reverse)
|
|
? returnValue.map((e: {value: string, affix: string}) => `${e.affix} (${e.value})`)
|
|
: returnValue.map((e: {value: string, affix: string}) => `${e.value}: ${e.affix}`)
|
|
}
|
|
}
|
|
|
|
// Build string out of single-relationship
|
|
if ((
|
|
field.type === "singleToManyRelationship" ||
|
|
field.type === "singleToSingleRelationship" ||
|
|
field.type === "singleToNoneRelationship"
|
|
) && returnValue && returnValue.value
|
|
) {
|
|
const valueToMap = Array.isArray(returnValue.value) ? returnValue.value[0] : returnValue.value
|
|
|
|
// @ts-ignore
|
|
const matchingDocument = this.SGET_allDocuments.docs.find(doc => doc.id === valueToMap._id)
|
|
|
|
if (matchingDocument) {
|
|
// @ts-ignore
|
|
let localReturnValue = matchingDocument.extraFields.find(e => e.id === "name")?.value as string
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
const matchedNote = returnValue?.addedValues
|
|
if (matchedNote?.value?.length > 0) {
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
localReturnValue = `${localReturnValue} (${matchedNote.value})`
|
|
}
|
|
|
|
returnValue = localReturnValue
|
|
}
|
|
else {
|
|
returnValue = ""
|
|
}
|
|
}
|
|
|
|
// Build string out of multi-relationship
|
|
if ((
|
|
field.type === "manyToManyRelationship" ||
|
|
field.type === "manyToSingleRelationship" ||
|
|
field.type === "manyToNoneRelationship"
|
|
) && returnValue && returnValue.value
|
|
) {
|
|
const valuesToMap = returnValue.value as {_id: string, type: string}[]
|
|
|
|
const mappedValues = valuesToMap
|
|
.filter(value => {
|
|
return value.type === field?.relationshipSettings?.connectedObjectType
|
|
})
|
|
.map(value => {
|
|
// @ts-ignore
|
|
const matchingDocument = this.SGET_allDocuments.docs.find(doc => doc.id === value._id)
|
|
if (matchingDocument) {
|
|
// @ts-ignore
|
|
let localReturnValue = matchingDocument.extraFields.find(e => e.id === "name")?.value as string
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
const matchedNote = returnValue?.addedValues?.find((e: {pairedId: string}) => e.pairedId === value._id)
|
|
if (matchedNote?.value?.length > 0) {
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
localReturnValue = `${localReturnValue} (${matchedNote.value})`
|
|
}
|
|
|
|
return localReturnValue
|
|
}
|
|
return " "
|
|
})
|
|
.filter(e => e !== " ")
|
|
|
|
returnValue = mappedValues
|
|
}
|
|
|
|
// Fix all missing values that slipped through
|
|
if (returnValue === undefined) {
|
|
returnValue = ""
|
|
}
|
|
|
|
const returnValueFormat = returnValue as string
|
|
|
|
const name = field.name
|
|
|
|
return {
|
|
label: name,
|
|
value: returnValueFormat,
|
|
type: field.type,
|
|
id: field.id
|
|
}
|
|
})
|
|
.filter(field => field.value.length > 0 || field.type === "break")
|
|
|
|
// Map empty breaks
|
|
const idsToRemove = []
|
|
for (let index = 0; index < mappedFields.length; index++) {
|
|
const field = mappedFields[index]
|
|
|
|
if (field.type === "break" && mappedFields[index + 1]?.type === "break") {
|
|
idsToRemove.push(field.id)
|
|
}
|
|
}
|
|
|
|
// Remove empty breaks
|
|
idsToRemove.forEach(id => {
|
|
const indexToRemove = mappedFields.findIndex(field => field.id === id)
|
|
|
|
if (indexToRemove > -1) {
|
|
mappedFields.splice(indexToRemove, 1)
|
|
}
|
|
})
|
|
|
|
// Check for last, hanging break field
|
|
if (mappedFields[mappedFields.length - 1]?.type === "break") {
|
|
mappedFields.splice(mappedFields.length - 1, 1)
|
|
}
|
|
|
|
return mappedFields
|
|
}
|
|
|
|
fixExportPaths (exportPath: string, input: I_ExportObject) {
|
|
const reservedCharacterList = [
|
|
"/",
|
|
">",
|
|
"<",
|
|
"|",
|
|
":",
|
|
"&",
|
|
"\\",
|
|
"-",
|
|
"[",
|
|
"]",
|
|
"{",
|
|
"}",
|
|
"*",
|
|
"?",
|
|
"'",
|
|
"\"",
|
|
"#",
|
|
"%",
|
|
"$",
|
|
"!",
|
|
"@"
|
|
]
|
|
|
|
let exportFileDirectory = input.documentDirectory
|
|
reservedCharacterList.forEach(char => {
|
|
exportFileDirectory = exportFileDirectory.replace(char, "-")
|
|
exportFileDirectory = exportFileDirectory.replace(char, "-")
|
|
exportFileDirectory = exportFileDirectory.replace(char, "-")
|
|
exportFileDirectory = exportFileDirectory.replace(char, "-")
|
|
exportFileDirectory = exportFileDirectory.replace(char, "-")
|
|
exportFileDirectory = exportFileDirectory.replace(char, "-")
|
|
})
|
|
const documentDirectory = `${exportPath}/${exportFileDirectory}`
|
|
|
|
// Create directory
|
|
if (!fs.existsSync(documentDirectory)) {
|
|
fs.mkdirSync(documentDirectory)
|
|
}
|
|
|
|
// Fix invalid characters in document file name
|
|
let exportFileName = input.name
|
|
reservedCharacterList.forEach(char => {
|
|
exportFileName = exportFileName.replace(char, "-")
|
|
exportFileName = exportFileName.replace(char, "-")
|
|
exportFileName = exportFileName.replace(char, "-")
|
|
exportFileName = exportFileName.replace(char, "-")
|
|
exportFileName = exportFileName.replace(char, "-")
|
|
exportFileName = exportFileName.replace(char, "-")
|
|
})
|
|
|
|
if (input.isCategory) {
|
|
exportFileName = `_${exportFileName}`
|
|
}
|
|
|
|
return { documentDirectory, exportFileName }
|
|
}
|
|
|
|
exportFile_MD (input: I_ExportObject, exportPath: string) {
|
|
// Build the proper JSON file for export
|
|
const JSONExport: any[] = []
|
|
|
|
// Name/Title
|
|
JSONExport.push({ h1: input.name })
|
|
if (input.isCategory) {
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
JSONExport[0] = `${JSONExport[0]} - Category`
|
|
}
|
|
|
|
// Document type
|
|
JSONExport.push({ h2: "Document type" })
|
|
JSONExport.push({ ul: [input.documentType] })
|
|
|
|
// Status
|
|
if (!this.hideDeadInformation) {
|
|
JSONExport.push({ h2: "Status" })
|
|
JSONExport.push({ ul: [(input.isDead) ? "Dead/Gone/Destroyed" : "Active/Alive"] })
|
|
}
|
|
|
|
// Hierarchy path
|
|
if (this.includeHierarchyPath) {
|
|
JSONExport.push({ h2: "Hierarchical path" })
|
|
JSONExport.push({ ul: [input.hierarchicalPath] })
|
|
}
|
|
|
|
// Tags
|
|
if (this.includeTags) {
|
|
JSONExport.push({ h2: "Tags" })
|
|
JSONExport.push({ ul: (Array.isArray(input.tags) ? input.tags : []) })
|
|
}
|
|
|
|
// Other fields
|
|
input.fieldValues.forEach(field => {
|
|
if (field.type === "break") {
|
|
JSONExport.push({ hr: "" })
|
|
JSONExport.push({ h1: field.label })
|
|
}
|
|
else if (field.type === "wysiwyg") {
|
|
JSONExport.push({ h2: field.label })
|
|
JSONExport.push({ p: field.value })
|
|
}
|
|
else {
|
|
JSONExport.push({ h2: field.label })
|
|
if (Array.isArray(field.value)) {
|
|
JSONExport.push({ ul: field.value })
|
|
}
|
|
else {
|
|
JSONExport.push({ ul: [field.value] })
|
|
}
|
|
}
|
|
})
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
let mdContent: string = json2md(JSONExport)
|
|
|
|
// Fix double-empty lines
|
|
var EOL = mdContent.match(/\r\n/gm) ? "\r\n" : "\n"
|
|
var regExp = new RegExp("(" + EOL + "){3,}", "gm")
|
|
mdContent = mdContent.replace(regExp, EOL + EOL)
|
|
|
|
const { documentDirectory, exportFileName } = this.fixExportPaths(exportPath, input)
|
|
|
|
// Write the file
|
|
fs.writeFileSync(`${documentDirectory}/${exportFileName}.md`, mdContent)
|
|
}
|
|
|
|
exportFile_PDF (input: I_ExportObject, exportPath: string) {
|
|
const { documentDirectory, exportFileName } = this.fixExportPaths(exportPath, input)
|
|
|
|
const textFont = 11
|
|
const subTitleFont = 15
|
|
const listPadding = 60
|
|
const textPadding = 40
|
|
const blockquotePadding = 85
|
|
|
|
const paragraphOptions = {
|
|
lineGap: 3,
|
|
paragraphGap: 8
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
const doc: I_PDFKitDocument = new PDFkit({ size: "A4" })
|
|
|
|
// Start stream
|
|
doc.pipe(fs.createWriteStream(`${documentDirectory}/${exportFileName}.pdf`))
|
|
|
|
// Name/Title
|
|
let title = input.name
|
|
if (input.isCategory) {
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
title = `${title} - Category`
|
|
}
|
|
doc.font("Times-Bold").fillColor("#18303a").fontSize(20)
|
|
.text(title, { align: "center" })
|
|
|
|
// Next line
|
|
doc.fontSize(textFont).moveDown().moveDown()
|
|
|
|
// Document type
|
|
doc.font("Times-Bold").fillColor("#000000").fontSize(textFont)
|
|
.text("Document type", textPadding, undefined, paragraphOptions)
|
|
doc.font("Times-Roman").fillColor("#000000").fontSize(textFont)
|
|
.list([input.documentType], listPadding, undefined, paragraphOptions)
|
|
.moveDown()
|
|
|
|
// Status
|
|
if (!this.hideDeadInformation) {
|
|
doc.font("Times-Bold").fillColor("#000000").fontSize(textFont)
|
|
.text("Status", textPadding, undefined, paragraphOptions)
|
|
doc.font("Times-Roman").fillColor("#000000").fontSize(textFont)
|
|
.list([((input.isDead) ? "Dead/Gone/Destroyed" : "Active/Alive")], listPadding, undefined, paragraphOptions)
|
|
.moveDown()
|
|
}
|
|
|
|
// Hierarchy path
|
|
if (this.includeHierarchyPath) {
|
|
doc.font("Times-Bold").fillColor("#000000").fontSize(textFont)
|
|
.text("Hierarchical path", textPadding, undefined, paragraphOptions)
|
|
doc.font("Times-Roman").fillColor("#000000").fontSize(textFont)
|
|
.list([input.hierarchicalPath], listPadding)
|
|
.moveDown()
|
|
}
|
|
|
|
// Tags
|
|
if (this.includeTags) {
|
|
doc.font("Times-Bold").fillColor("#000000").fontSize(textFont)
|
|
.text("Tags", textPadding, undefined, paragraphOptions)
|
|
doc.font("Times-Roman").fillColor("#000000").fontSize(textFont)
|
|
.list((Array.isArray(input.tags) ? input.tags : []), listPadding, undefined, paragraphOptions)
|
|
.moveDown()
|
|
}
|
|
|
|
// Other fields
|
|
input.fieldValues.forEach(field => {
|
|
if (field.type === "break") {
|
|
doc.moveDown()
|
|
.font("Times-Bold").fillColor("#000000").fontSize(subTitleFont)
|
|
.text(field.label, textPadding, undefined, paragraphOptions)
|
|
.moveDown()
|
|
}
|
|
else if (field.type === "wysiwyg") {
|
|
doc.font("Times-Bold").fillColor("#000000").fontSize(textFont)
|
|
.text(field.label, textPadding, undefined, paragraphOptions)
|
|
.moveDown()
|
|
|
|
// @ts-ignore
|
|
const returnList = this.buildPDFWysiwygContent(field.value)
|
|
|
|
doc.font("Times-Roman").fillColor("#000000").fontSize(textFont)
|
|
|
|
const wysiwygOptions: {[key:string]: any} = extend(true, {}, paragraphOptions)
|
|
|
|
returnList.forEach(node => {
|
|
if (node.type === "text") {
|
|
// Italic
|
|
wysiwygOptions.oblique = node.attrs.italic
|
|
|
|
// Underline
|
|
wysiwygOptions.underline = node.attrs.underline
|
|
|
|
// Bold
|
|
doc.font((node?.attrs?.bold) ? "Times-Bold" : "Times-Roman")
|
|
|
|
// Continued
|
|
wysiwygOptions.continued = node.attrs.continued
|
|
|
|
// Align
|
|
wysiwygOptions.align = (node?.attrs?.align) ? node.attrs.align : "left"
|
|
// Padding
|
|
const wysiwygPadding = (node?.attrs?.blockquotePadding) ? blockquotePadding : listPadding
|
|
|
|
// @ts-ignore
|
|
doc.text(node.content, wysiwygPadding, undefined, wysiwygOptions)
|
|
}
|
|
if (node.type === "br") {
|
|
doc.moveDown()
|
|
}
|
|
})
|
|
// @ts-ignore
|
|
doc.moveDown()
|
|
}
|
|
else {
|
|
doc.font("Times-Bold").fillColor("#000000").fontSize(textFont)
|
|
.text(field.label, textPadding, undefined, paragraphOptions)
|
|
doc.font("Times-Roman").fillColor("#000000").fontSize(textFont)
|
|
.list((Array.isArray(field.value) ? field.value : [field.value]), listPadding, undefined, paragraphOptions)
|
|
.moveDown()
|
|
}
|
|
})
|
|
|
|
doc.end()
|
|
}
|
|
|
|
buildPDFWysiwygContent (input: string) {
|
|
const returnNodeList: I_HtmlParserNode[] = []
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
const parsedHTML: I_HtmlParserNode = htmlParseStringify.parse(`<div>${input}</div>`)
|
|
|
|
const processNodeStyles = (styleSting: string) => {
|
|
// eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
|
|
const alignMatch = styleSting.match(/text-align:\s*([^;}]*)/)
|
|
if (alignMatch) {
|
|
return alignMatch[1]
|
|
}
|
|
else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const processNode = (node: I_HtmlParserNode) => {
|
|
// ------------- NODE EXTRA ATTRIBUTES ------------------
|
|
let nodeStyles: false|string = false
|
|
if (node?.attrs?.style) {
|
|
const snapshot: {style:string} = extend(true, {}, node.attrs)
|
|
nodeStyles = (processNodeStyles(snapshot.style)) ? snapshot.style : false
|
|
}
|
|
|
|
let parentIsBlockquote = false
|
|
if (node.parentNode?.attrs?.blockquotePadding) {
|
|
parentIsBlockquote = true
|
|
}
|
|
|
|
node.attrs = {}
|
|
node.attrs.continued = false
|
|
|
|
// @ts-ignore
|
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
const nextNode = node.selfNodeList[node.selfIndex + 1]
|
|
|
|
// If next if bold, italic or underline
|
|
if (nextNode) {
|
|
if ((nextNode.type === "tag" && nextNode.name === "i") ||
|
|
(nextNode.type === "tag" && nextNode.name === "b") ||
|
|
(nextNode.type === "tag" && nextNode.name === "u")
|
|
) {
|
|
node.attrs.continued = true
|
|
}
|
|
}
|
|
|
|
// Text align
|
|
if (nodeStyles) {
|
|
const textAlign = processNodeStyles(nodeStyles)
|
|
if (textAlign && textAlign !== "left") {
|
|
node.attrs.align = textAlign
|
|
}
|
|
}
|
|
else if (node.parentNode?.attrs?.align && node.parentNode?.attrs?.align !== "left") {
|
|
node.attrs.align = node.parentNode.attrs.align
|
|
}
|
|
|
|
// Text modifier - Italic
|
|
if ((node.type === "tag" && node.name === "i") || node?.parentNode?.attrs.italic === true) {
|
|
node.attrs.italic = true
|
|
node.attrs.continued = true
|
|
}
|
|
else {
|
|
node.attrs.italic = false
|
|
}
|
|
|
|
// Text modifier - Bold
|
|
if ((node.type === "tag" && node.name === "b") || node?.parentNode?.attrs.bold === true) {
|
|
node.attrs.bold = true
|
|
node.attrs.continued = true
|
|
}
|
|
else {
|
|
node.attrs.bold = false
|
|
}
|
|
|
|
// Text modifier - Underline
|
|
if ((node.type === "tag" && node.name === "u") || node?.parentNode?.attrs.underline === true) {
|
|
node.attrs.underline = true
|
|
node.attrs.continued = true
|
|
}
|
|
else {
|
|
node.attrs.underline = false
|
|
}
|
|
|
|
// If it is the last one, obviously dont continue anything
|
|
if (!nextNode && node.type !== "text") {
|
|
node.isLast = true
|
|
}
|
|
|
|
// Don't continue is this lack a continuing node
|
|
if (node.parentNode?.isLast && !nextNode) {
|
|
node.attrs.continued = false
|
|
}
|
|
|
|
// Extra padding for blockquotes
|
|
if ((node.type === "tag" && node.name === "blockquote") || parentIsBlockquote) {
|
|
node.attrs.blockquotePadding = true
|
|
}
|
|
|
|
// ------------- NODE PROCESSING ----------------------
|
|
|
|
// Return text node value OR a break
|
|
if ((node.type === "text" && node.content) || node.type === "br") {
|
|
const returnNode = node
|
|
// @ts-ignore
|
|
returnNode.content = returnNode.content.replace(/ /g, "").replace(/(\r\n|\n|\r)/gm, "")
|
|
returnNodeList.push(returnNode)
|
|
}
|
|
|
|
// Process subnodes
|
|
else if (node?.children?.length > 0) {
|
|
node.children.forEach((childNode, i) => {
|
|
childNode.selfIndex = i
|
|
childNode.selfNodeList = node.children
|
|
childNode.parentNode = node
|
|
|
|
processNode(childNode)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Generate return value
|
|
parsedHTML[0].selfNodeList = [parsedHTML[0]]
|
|
parsedHTML[0].selfIndex = 0
|
|
parsedHTML[0].attrs = {}
|
|
processNode(parsedHTML[0])
|
|
|
|
return returnNodeList
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.exportDialog {
|
|
width: 1000px;
|
|
max-width: calc(100vw - 100px) !important;
|
|
margin-top: 100px;
|
|
align-self: flex-start;
|
|
|
|
h6 {
|
|
display: block;
|
|
}
|
|
}
|
|
</style>
|