991 lines
32 KiB
Vue
991 lines
32 KiB
Vue
<template>
|
|
<div class="mt-6">
|
|
<div>
|
|
<v-btn depressed tile @click="toggleReveal">
|
|
Wiki<v-icon v-text="reveal ? '$ayiEyeSlash' : '$ayiEye'" right></v-icon
|
|
></v-btn>
|
|
</div>
|
|
<template v-if="reveal">
|
|
<template v-if="!readonly">
|
|
<div class="mt-2">
|
|
<v-btn
|
|
text
|
|
:outlined="currentView == view.WIKI_VIEW"
|
|
@click="currentView = view.WIKI_VIEW"
|
|
>
|
|
<v-icon>$ayiEye</v-icon>
|
|
</v-btn>
|
|
<v-btn
|
|
text
|
|
:outlined="currentView == view.DESIGN_VIEW"
|
|
@click="currentView = view.DESIGN_VIEW"
|
|
>
|
|
<v-icon>$ayiEdit</v-icon>
|
|
</v-btn>
|
|
<v-btn
|
|
text
|
|
:outlined="currentView == view.SPLIT_VIEW"
|
|
@click="currentView = view.SPLIT_VIEW"
|
|
>
|
|
<v-icon>$ayiColumns</v-icon>
|
|
</v-btn>
|
|
</div>
|
|
</template>
|
|
<v-sheet
|
|
v-if="currentView != this.view.HIDDEN_VIEW"
|
|
elevation="2"
|
|
class="aywiki pa-2 pa-sm-6 mt-2"
|
|
>
|
|
<v-row v-resize="onResize">
|
|
<!-- BUTTONS -->
|
|
<v-col v-if="showDesigner()" :cols="12">
|
|
<div>
|
|
<v-btn depressed tile @click="editClick('bold')">
|
|
<v-icon>$ayiBold</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('italic')">
|
|
<v-icon>$ayiItalic</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('strike')">
|
|
<v-icon>$ayiStrikethrough</v-icon></v-btn
|
|
>
|
|
|
|
<!-- HEADINGS -->
|
|
<v-menu offset-y>
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn depressed tile v-on="on">
|
|
<v-icon>$ayiHeading</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item @click="editClick('heading', 1)">
|
|
<v-list-item-title
|
|
><h1>{{ $ay.t("Heading") }} 1</h1></v-list-item-title
|
|
>
|
|
</v-list-item>
|
|
<v-list-item @click="editClick('heading', 2)">
|
|
<v-list-item-title
|
|
><h2>{{ $ay.t("Heading") }} 2</h2></v-list-item-title
|
|
>
|
|
</v-list-item>
|
|
<v-list-item @click="editClick('heading', 3)">
|
|
<v-list-item-title
|
|
><h3>{{ $ay.t("Heading") }} 3</h3></v-list-item-title
|
|
>
|
|
</v-list-item>
|
|
<v-list-item @click="editClick('heading', 4)">
|
|
<v-list-item-title
|
|
><h4>{{ $ay.t("Heading") }} 4</h4></v-list-item-title
|
|
>
|
|
</v-list-item>
|
|
<v-list-item @click="editClick('heading', 5)">
|
|
<v-list-item-title
|
|
><h5>{{ $ay.t("Heading") }} 5</h5></v-list-item-title
|
|
>
|
|
</v-list-item>
|
|
<v-list-item @click="editClick('heading', 6)">
|
|
<v-list-item-title
|
|
><h6>{{ $ay.t("Heading") }} 6</h6></v-list-item-title
|
|
>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
<!-- /HEADINGS -->
|
|
|
|
<!-- MORE BUTTONS -->
|
|
<v-btn depressed tile class="ml-1" @click="editClick('line')">
|
|
<v-icon>$ayiMinus</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('quote')">
|
|
<v-icon>$ayiQuoteLeft</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('ul')">
|
|
<v-icon>$ayiListUl</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('ol')">
|
|
<v-icon>$ayiListOl</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('task')">
|
|
<v-icon>$ayiCheckSquare</v-icon></v-btn
|
|
>
|
|
|
|
<!-- TABLES -->
|
|
<v-menu
|
|
v-model="tableMenu"
|
|
:close-on-content-click="false"
|
|
offset-y
|
|
>
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn depressed tile class="ml-1" v-on="on">
|
|
<v-icon>$ayiTable</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<v-card width="300">
|
|
<v-card-title>{{ $ay.t("Table") }}</v-card-title>
|
|
|
|
<div class="ma-8">
|
|
<v-slider
|
|
thumb-size="24"
|
|
thumb-label="always"
|
|
v-model="tableMenuColumns"
|
|
min="1"
|
|
max="10"
|
|
prepend-icon="$ayiArrowsAltH"
|
|
></v-slider>
|
|
|
|
<v-slider
|
|
prepend-icon="$ayiArrowsAltV"
|
|
class="mt-8"
|
|
thumb-size="24"
|
|
thumb-label="always"
|
|
v-model="tableMenuRows"
|
|
min="1"
|
|
max="15"
|
|
></v-slider>
|
|
</div>
|
|
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
|
|
<v-btn text @click="tableMenu = false">{{
|
|
$ay.t("Cancel")
|
|
}}</v-btn>
|
|
<v-btn color="primary" text @click="editClick('table')">{{
|
|
$ay.t("OK")
|
|
}}</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-menu>
|
|
<!-- /TABLES -->
|
|
|
|
<!-- LINK -->
|
|
<v-btn depressed tile @click="openLinkMenu">
|
|
<v-icon>$ayiLink</v-icon>
|
|
</v-btn>
|
|
<v-menu
|
|
min-width="300"
|
|
v-model="linkMenu"
|
|
:close-on-content-click="false"
|
|
offset-y
|
|
:position-x="menuX"
|
|
:position-y="menuY"
|
|
absolute
|
|
>
|
|
<v-card width="300">
|
|
<v-card-title>{{ $ay.t("InsertLink") }}</v-card-title>
|
|
|
|
<div class="ma-8">
|
|
<v-text-field
|
|
v-model="linkUrl"
|
|
:label="$ay.t('LinkUrl')"
|
|
></v-text-field>
|
|
<v-text-field
|
|
v-model="linkText"
|
|
:label="$ay.t('LinkText')"
|
|
></v-text-field>
|
|
</div>
|
|
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
|
|
<v-btn text @click="linkMenu = false">{{
|
|
$ay.t("Cancel")
|
|
}}</v-btn>
|
|
<v-btn color="primary" text @click="editClick('link')">{{
|
|
$ay.t("OK")
|
|
}}</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-menu>
|
|
<!-- /LINKS -->
|
|
|
|
<!-- IMAGE -->
|
|
|
|
<v-btn depressed tile @click="openImageMenu">
|
|
<v-icon>$ayiImage</v-icon>
|
|
</v-btn>
|
|
<v-menu
|
|
min-width="360"
|
|
v-model="imageMenu"
|
|
:close-on-content-click="false"
|
|
offset-y
|
|
:position-x="menuX"
|
|
:position-y="menuY"
|
|
absolute
|
|
>
|
|
<v-card>
|
|
<v-card-title>{{ $ay.t("InsertImage") }}</v-card-title>
|
|
<div>
|
|
<v-tabs v-model="imageTab" grow color="primary">
|
|
<v-tab key="url">URL</v-tab>
|
|
<v-tab key="file">{{ $ay.t("Attachments") }}</v-tab>
|
|
<v-tabs-items v-model="imageTab">
|
|
<v-tab-item key="url"
|
|
><div class="ma-6">
|
|
<v-text-field
|
|
v-model="imageUrl"
|
|
:label="$ay.t('ImageUrl')"
|
|
></v-text-field>
|
|
<v-text-field
|
|
v-model="imageText"
|
|
:label="$ay.t('ImageDescription')"
|
|
></v-text-field></div
|
|
></v-tab-item>
|
|
<v-tab-item key="file"
|
|
><div class="ma-6">
|
|
<v-select
|
|
@click="getAttachments"
|
|
:label="$ay.t('Attachments')"
|
|
v-model="selectedImageAttachment"
|
|
:items="attachments"
|
|
item-text="name"
|
|
item-value="id"
|
|
return-object
|
|
></v-select>
|
|
|
|
<v-text-field
|
|
v-model="imageText"
|
|
:label="$ay.t('ImageDescription')"
|
|
></v-text-field></div
|
|
></v-tab-item>
|
|
</v-tabs-items>
|
|
</v-tabs>
|
|
</div>
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
<v-btn text @click="imageMenu = false">{{
|
|
$ay.t("Cancel")
|
|
}}</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
text
|
|
@click="editClick('image', imageTab)"
|
|
>{{ $ay.t("OK") }}</v-btn
|
|
>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-menu>
|
|
|
|
<!-- /IMAGE -->
|
|
|
|
<!-- CODE -->
|
|
<v-btn depressed tile class="ml-1" @click="editClick('code')">
|
|
<v-icon>$ayiCode</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('codeblock')">
|
|
<v-icon>$ayiSquareFull</v-icon></v-btn
|
|
>
|
|
|
|
<!-- HELP -->
|
|
<v-btn depressed tile class="ml-1" @click="goHelp()">
|
|
<v-icon>$ayiQuestionCircle</v-icon></v-btn
|
|
>
|
|
<!-- END OF BUTTONS -->
|
|
</div>
|
|
</v-col>
|
|
<!-- DESIGNER -->
|
|
<v-col
|
|
v-if="showDesigner()"
|
|
:cols="currentView == view.SPLIT_VIEW ? 6 : 12"
|
|
>
|
|
<div :style="editStyle()">
|
|
<v-textarea
|
|
v-cloak
|
|
@drop.prevent="onDrop"
|
|
@dragover.prevent
|
|
solo
|
|
no-resize
|
|
:height="editAreaHeight"
|
|
ref="editArea"
|
|
@input="handleInput"
|
|
@dblclick="handleDoubleClick"
|
|
v-model="localVal"
|
|
></v-textarea>
|
|
</div>
|
|
</v-col>
|
|
<!-- WIKI auto-grow-->
|
|
<v-col
|
|
v-if="showWiki()"
|
|
:cols="currentView == view.SPLIT_VIEW ? 6 : 12"
|
|
>
|
|
<div
|
|
:style="wikiStyle()"
|
|
class="markdown-body"
|
|
v-html="compiledOutput()"
|
|
></div>
|
|
</v-col>
|
|
</v-row>
|
|
<v-btn depressed tile @click="toggleReveal">
|
|
Wiki<v-icon
|
|
v-text="reveal ? '$ayiEyeSlash' : '$ayiEye'"
|
|
right
|
|
></v-icon
|
|
></v-btn>
|
|
</v-sheet>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import marked from "marked";
|
|
import DOMPurify from "dompurify";
|
|
|
|
export default {
|
|
created() {
|
|
// Add a hook to make all links open a new window
|
|
//https://github.com/cure53/DOMPurify/blob/master/demos/hooks-target-blank-demo.html
|
|
DOMPurify.addHook("afterSanitizeAttributes", function(node) {
|
|
// set all elements owning target to target=_blank
|
|
if ("target" in node) {
|
|
node.setAttribute("target", "_blank");
|
|
// prevent https://www.owasp.org/index.php/Reverse_Tabnabbing
|
|
node.setAttribute("rel", "noopener noreferrer");
|
|
}
|
|
// set non-HTML/MathML links to xlink:show=new
|
|
if (
|
|
!node.hasAttribute("target") &&
|
|
(node.hasAttribute("xlink:href") || node.hasAttribute("href"))
|
|
) {
|
|
node.setAttribute("xlink:show", "new");
|
|
}
|
|
});
|
|
},
|
|
data() {
|
|
return {
|
|
localVal: this.value,
|
|
currentView: 0,
|
|
reveal: false,
|
|
selection: {
|
|
start: 0,
|
|
end: 0,
|
|
startOfBlock: 0, //block meaning first character after last linefeed before or at start of selection
|
|
endOfBlock: 0, //end of block meaning selection expanded to end of line (unless there isn't one)
|
|
hasSelection: false
|
|
},
|
|
editAreaHeight: 300,
|
|
view: {
|
|
HIDDEN_VIEW: 0,
|
|
SPLIT_VIEW: 1,
|
|
WIKI_VIEW: 2,
|
|
DESIGN_VIEW: 3
|
|
},
|
|
tableMenu: false,
|
|
tableMenuColumns: 2,
|
|
tableMenuRows: 2,
|
|
linkMenu: false,
|
|
menuX: 10,
|
|
menuY: 10,
|
|
linkUrl: "",
|
|
linkText: "",
|
|
imageMenu: false,
|
|
imageTab: null,
|
|
imageUrl: "",
|
|
imageText: "",
|
|
attachments: [],
|
|
selectedImageAttachment: null,
|
|
notes: null, //attachment upload notes
|
|
uploadFiles: [] //attachment upload files
|
|
};
|
|
},
|
|
props: {
|
|
value: { type: String, default: "" },
|
|
ayaType: Number,
|
|
ayaId: Number,
|
|
readonly: Boolean
|
|
},
|
|
watch: {
|
|
value(value) {
|
|
this.localVal = value ?? "";
|
|
}
|
|
},
|
|
methods: {
|
|
goHelp() {
|
|
window.open(this.$store.state.helpUrl + "ay-start-form-wiki", "_blank");
|
|
},
|
|
compiledOutput() {
|
|
if (!this.localVal) {
|
|
return "";
|
|
}
|
|
//replace attachment urls with tokenized local urls
|
|
let src = this.localVal.replace(/\[ATTACH:(.*)\]/g, function(match, p1) {
|
|
return window.$gz.api.attachmentDownloadUrl(p1);
|
|
});
|
|
|
|
return DOMPurify.sanitize(marked(src, { breaks: true }));
|
|
},
|
|
onResize() {
|
|
// this.editAreaHeight = window.innerHeight / 2;
|
|
this.editAreaHeight = window.innerHeight * 0.8;
|
|
},
|
|
toggleReveal() {
|
|
this.reveal = !this.reveal;
|
|
if (this.reveal) {
|
|
this.currentView = this.view.WIKI_VIEW;
|
|
}
|
|
},
|
|
editStyle() {
|
|
if (this.currentView == this.view.SPLIT_VIEW) {
|
|
return "height: " + this.editAreaHeight + "px;";
|
|
} else {
|
|
return false; //false attributes don't get rendered
|
|
}
|
|
},
|
|
wikiStyle() {
|
|
if (this.currentView == this.view.SPLIT_VIEW) {
|
|
return "height: " + this.editAreaHeight + "px;overflow-y:auto;";
|
|
} else {
|
|
return false; //false attributes don't get rendered
|
|
}
|
|
},
|
|
getSelectedRange(forceBlock) {
|
|
let bodyTextArea = this.$refs.editArea.$el.querySelector("textarea");
|
|
this.selection.start = bodyTextArea.selectionStart;
|
|
this.selection.end = bodyTextArea.selectionEnd;
|
|
|
|
//some edits only work on a block so if user is just clicked on a line then add make a selection
|
|
if (forceBlock) {
|
|
//add a character to selection forward if possible but if not then go backward one
|
|
if (this.selection.end < this.localVal.length) {
|
|
this.selection.end++;
|
|
} else {
|
|
if (this.selection.start > 0) {
|
|
this.selection.start--;
|
|
}
|
|
}
|
|
}
|
|
this.selection.hasSelection = this.selection.start != this.selection.end;
|
|
|
|
//block selection
|
|
//start of block...
|
|
//default
|
|
this.selection.startOfBlock = this.selection.start;
|
|
if (this.selection.start > 0) {
|
|
//find linefeed prior to current start
|
|
let indexOfLineFeed = this.localVal.lastIndexOf(
|
|
"\n",
|
|
this.selection.start
|
|
);
|
|
if (indexOfLineFeed != -1) {
|
|
this.selection.startOfBlock = indexOfLineFeed + 1;
|
|
} else {
|
|
//this may be wrong but if it's the first line then I think the block should be the first character
|
|
this.selection.startOfBlock = 0;
|
|
}
|
|
}
|
|
|
|
//end of block
|
|
//default
|
|
this.selection.endOfBlock = this.selection.end;
|
|
if (this.selection.end > 0) {
|
|
//find linefeed prior to current start
|
|
let indexOfLineFeed = this.localVal.indexOf("\n", this.selection.end);
|
|
if (indexOfLineFeed != -1) {
|
|
this.selection.endOfBlock = indexOfLineFeed;
|
|
}
|
|
}
|
|
},
|
|
setSelectedRange(start, end) {
|
|
let bodyTextArea = this.$refs.editArea.$el.querySelector("textarea");
|
|
bodyTextArea.setSelectionRange(start, end);
|
|
},
|
|
getSelectedText() {
|
|
let selectedText = "";
|
|
if (this.selection.hasSelection) {
|
|
selectedText = this.localVal.substring(
|
|
this.selection.start,
|
|
this.selection.end
|
|
);
|
|
}
|
|
return selectedText;
|
|
},
|
|
replaceSelectedText(newString) {
|
|
this.localVal = window.$gz.util.stringSplice(
|
|
this.localVal,
|
|
this.selection.start,
|
|
this.selection.end - this.selection.start,
|
|
newString
|
|
);
|
|
//emit input event to parent form for dirty tracking
|
|
this.handleInput(this.localVal);
|
|
},
|
|
getSelectedBlock() {
|
|
let selectedText = "";
|
|
if (this.selection.hasSelection) {
|
|
selectedText = this.localVal.substring(
|
|
this.selection.startOfBlock,
|
|
this.selection.endOfBlock
|
|
);
|
|
}
|
|
return selectedText; //.trim();
|
|
},
|
|
replaceSelectedBlock(newString) {
|
|
this.localVal = window.$gz.util.stringSplice(
|
|
this.localVal,
|
|
this.selection.startOfBlock,
|
|
this.selection.endOfBlock - this.selection.startOfBlock,
|
|
newString
|
|
);
|
|
//emit input event to parent form for dirty tracking
|
|
this.handleInput(this.localVal);
|
|
},
|
|
handleDoubleClick(i) {
|
|
//the purpose of this is only to change the selection if it's got an extra space to the right
|
|
//because double clicking on a word with another word after it causes the space to be included
|
|
this.getSelectedRange();
|
|
let temp = this.getSelectedText();
|
|
let tempTrimmed = temp.trimEnd();
|
|
let diff = temp.length - tempTrimmed.length;
|
|
if (diff != 0) {
|
|
//there were some spaces so update the selection range
|
|
//force selection to be shorter by diff
|
|
this.setSelectedRange(this.selection.start, this.selection.end - diff);
|
|
}
|
|
},
|
|
handleInput(val) {
|
|
this.$emit("input", val);
|
|
this.localVal = val;
|
|
},
|
|
switchViewIcon() {
|
|
//return the icon that indicates what it will change to if you click it
|
|
//mirror of switchview below
|
|
|
|
if (this.readonly) {
|
|
if (this.currentView == this.view.HIDDEN_VIEW) {
|
|
return "$ayiEye";
|
|
} else {
|
|
return "$ayiEyeSlash";
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (this.currentView) {
|
|
case this.view.HIDDEN_VIEW:
|
|
return "$ayiEye";
|
|
case this.view.WIKI_VIEW:
|
|
return "$ayiColumns";
|
|
case this.view.SPLIT_VIEW:
|
|
return "$ayiFeather";
|
|
case this.view.DESIGN_VIEW:
|
|
return "$ayiEye";
|
|
}
|
|
},
|
|
showWiki() {
|
|
return (
|
|
this.currentView == this.view.WIKI_VIEW ||
|
|
this.currentView == this.view.SPLIT_VIEW
|
|
);
|
|
},
|
|
showDesigner() {
|
|
return (
|
|
this.currentView == this.view.DESIGN_VIEW ||
|
|
this.currentView == this.view.SPLIT_VIEW
|
|
);
|
|
},
|
|
switchView() {
|
|
//if user can't edit then cycle between hidden and wiki view
|
|
if (this.readonly) {
|
|
if (this.currentView == this.view.HIDDEN_VIEW) {
|
|
this.currentView = this.view.WIKI_VIEW;
|
|
} else {
|
|
this.currentView = this.view.HIDDEN_VIEW;
|
|
}
|
|
return;
|
|
}
|
|
//user can edit so switch OUT of hidden view but never into it
|
|
//and cycle between design, split and wiki views only
|
|
//never goes into hidden
|
|
switch (this.currentView) {
|
|
case this.view.HIDDEN_VIEW:
|
|
this.currentView = this.view.WIKI_VIEW;
|
|
break;
|
|
case this.view.WIKI_VIEW:
|
|
this.currentView = this.view.SPLIT_VIEW;
|
|
break;
|
|
case this.view.SPLIT_VIEW:
|
|
this.currentView = this.view.DESIGN_VIEW;
|
|
break;
|
|
case this.view.DESIGN_VIEW:
|
|
this.currentView = this.view.WIKI_VIEW;
|
|
break;
|
|
}
|
|
},
|
|
visibleIcon() {
|
|
return this.wikiVisible ? "$ayiEyeSlash" : "$ayiEye";
|
|
},
|
|
//EDITING
|
|
editClick(editType, ex) {
|
|
if (editType != "heading") {
|
|
this.getSelectedRange();
|
|
}
|
|
switch (editType) {
|
|
case "bold":
|
|
this.replaceSelectedText("**" + this.getSelectedText() + "**");
|
|
break;
|
|
case "italic":
|
|
this.replaceSelectedText("*" + this.getSelectedText() + "*");
|
|
break;
|
|
case "strike":
|
|
this.replaceSelectedText("~~" + this.getSelectedText() + "~~");
|
|
break;
|
|
case "line":
|
|
this.replaceSelectedText("***");
|
|
break;
|
|
case "heading":
|
|
{
|
|
this.getSelectedRange(true); //special forces
|
|
let prepend = "#".repeat(ex) + " ";
|
|
let s = this.getSelectedBlock();
|
|
s = s.replace(/\n/gi, "\n" + prepend);
|
|
if (s.length > 0 && s[0] != "\n") {
|
|
s = prepend + s;
|
|
}
|
|
this.replaceSelectedBlock(s);
|
|
}
|
|
break;
|
|
case "code":
|
|
this.replaceSelectedText("`" + this.getSelectedText() + "`");
|
|
break;
|
|
case "codeblock":
|
|
this.replaceSelectedBlock(
|
|
"\n```\n" + this.getSelectedBlock() + "\n```\n"
|
|
);
|
|
break;
|
|
case "quote":
|
|
if (this.selection.hasSelection) {
|
|
this.replaceSelectedBlock("\n>" + this.getSelectedBlock() + "\n");
|
|
} else {
|
|
this.replaceSelectedText("\n>");
|
|
}
|
|
break;
|
|
case "ul":
|
|
{
|
|
if (this.selection.hasSelection) {
|
|
let s = this.getSelectedBlock();
|
|
s = s.replace(/\n/gi, "\n* ");
|
|
if (s.length > 0 && s[0] != "\n") {
|
|
s = "* " + s;
|
|
}
|
|
s = s + "\n";
|
|
this.replaceSelectedBlock(s);
|
|
} else {
|
|
this.replaceSelectedText("\n* ");
|
|
}
|
|
}
|
|
break;
|
|
case "ol":
|
|
{
|
|
if (this.selection.hasSelection) {
|
|
let s = this.getSelectedBlock();
|
|
let ret = "\n1. ";
|
|
let listItem = 1;
|
|
for (let i = 0; i < s.length; i++) {
|
|
if (s[i] == "\n") {
|
|
++listItem;
|
|
ret += "\n" + listItem + ". ";
|
|
} else {
|
|
ret += s[i];
|
|
}
|
|
}
|
|
ret += "\n\n";
|
|
this.replaceSelectedBlock(ret);
|
|
} else {
|
|
this.replaceSelectedText("\n1. ");
|
|
}
|
|
}
|
|
break;
|
|
case "task":
|
|
{
|
|
if (this.selection.hasSelection) {
|
|
let s = this.getSelectedBlock();
|
|
s = s.replace(/\n/gi, "\n* [ ] ");
|
|
if (s.length > 0 && s[0] != "\n") {
|
|
s = "* [ ] " + s;
|
|
}
|
|
s = s + "\n";
|
|
this.replaceSelectedBlock(s);
|
|
} else {
|
|
this.replaceSelectedText("\n* [ ] ");
|
|
}
|
|
}
|
|
break;
|
|
case "table":
|
|
{
|
|
this.tableMenu = false;
|
|
//| Column 1 | Column 2 | Column 3 |
|
|
//| -------- | -------- | -------- |
|
|
//| John | Doe | Male |
|
|
//| Mary | Smith | Female |
|
|
this.getSelectedRange();
|
|
|
|
let t = "\n";
|
|
|
|
//Header
|
|
//| CCC | CCC | CCC |
|
|
for (let c = 0; c < this.tableMenuColumns; c++) {
|
|
t += "| CCC ";
|
|
}
|
|
t += "|\n";
|
|
|
|
//Divider
|
|
//| --- | --- | --- |
|
|
for (let c = 0; c < this.tableMenuColumns; c++) {
|
|
t += "| --- ";
|
|
}
|
|
t += "|\n";
|
|
|
|
//Rows
|
|
//| XXX | XXX | XXX |
|
|
for (let r = 0; r < this.tableMenuRows; r++) {
|
|
for (let c = 0; c < this.tableMenuColumns; c++) {
|
|
t += "| XXX ";
|
|
}
|
|
t += "|\n";
|
|
}
|
|
|
|
this.replaceSelectedText(t);
|
|
}
|
|
break;
|
|
case "link":
|
|
{
|
|
this.linkMenu = false;
|
|
let url = this.linkUrl;
|
|
if (!url) {
|
|
return;
|
|
}
|
|
//force it to a full url so it doesn't attempt to open it as a SPA window
|
|
if (!url.includes(":")) {
|
|
url = "https://" + url;
|
|
}
|
|
let t = "[" + this.linkText + "](" + url + ")";
|
|
// [MY Awesome LINK](www.ayanova.com)
|
|
this.replaceSelectedText(t);
|
|
}
|
|
break;
|
|
case "image":
|
|
//")
|
|
{
|
|
this.imageMenu = false;
|
|
|
|
let url = null;
|
|
if (ex == 1) {
|
|
if (
|
|
!this.selectedImageAttachment ||
|
|
!this.selectedImageAttachment.url
|
|
) {
|
|
return;
|
|
}
|
|
//it's an attachment
|
|
url = this.selectedImageAttachment.url;
|
|
if (!this.imageText) {
|
|
this.imageText = this.selectedImageAttachment.name;
|
|
}
|
|
} else {
|
|
//it's a url paste / manual entry
|
|
url = this.imageUrl;
|
|
}
|
|
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
let txt = this.imageText;
|
|
//force it to a full url so it doesn't attempt to open it as a SPA window
|
|
if (!url.includes(":")) {
|
|
url = "https://" + url;
|
|
}
|
|
|
|
if (txt) {
|
|
this.replaceSelectedText(
|
|
" \n' + txt //keep original selected text otherwise it will vanish
|
|
);
|
|
} else {
|
|
this.replaceSelectedText("");
|
|
}
|
|
this.selectedImageAttachment = null;
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error(`wiki-control: ${editType} NOT IMPLEMENTED`);
|
|
break;
|
|
}
|
|
|
|
// //emit input event to parent form for dirty tracking
|
|
// this.handleInput(this.localVal);
|
|
},
|
|
openLinkMenu(e) {
|
|
e.preventDefault();
|
|
this.linkMenu = false;
|
|
this.getSelectedRange();
|
|
this.linkText = this.getSelectedText();
|
|
this.menuX = e.clientX;
|
|
this.menuY = e.clientY;
|
|
this.$nextTick(() => {
|
|
this.linkMenu = true;
|
|
});
|
|
},
|
|
openImageMenu(e) {
|
|
e.preventDefault();
|
|
this.imageMenu = false;
|
|
this.getSelectedRange();
|
|
this.imageText = this.getSelectedText();
|
|
this.menuX = e.clientX;
|
|
this.menuY = e.clientY;
|
|
this.$nextTick(() => {
|
|
this.imageMenu = true;
|
|
});
|
|
},
|
|
async getAttachments() {
|
|
let vm = this;
|
|
try {
|
|
vm.attachments = [];
|
|
let res = await window.$gz.api.get(
|
|
"attachment/list?ayatype=" + vm.ayaType + "&ayaid=" + vm.ayaId
|
|
);
|
|
if (res.error) {
|
|
window.$gz.errorHandler.handleFormError(res.error);
|
|
} else {
|
|
let ret = [];
|
|
for (let i = 0; i < res.data.length; i++) {
|
|
let o = res.data[i];
|
|
|
|
if (
|
|
window.$gz.util.isImageAttachment(
|
|
o.displayFileName,
|
|
o.contentType
|
|
)
|
|
) {
|
|
//attach url
|
|
//
|
|
ret.push({
|
|
id: o.id,
|
|
url: "[ATTACH:" + o.id + "]",
|
|
name: o.displayFileName
|
|
});
|
|
}
|
|
}
|
|
vm.attachments = ret;
|
|
}
|
|
} catch (error) {
|
|
window.$gz.errorHandler.handleFormError(error);
|
|
}
|
|
},
|
|
async upload() {
|
|
//similar code in attachment-control upload
|
|
let vm = this;
|
|
let at = {
|
|
ayaId: vm.ayaId,
|
|
ayaType: vm.ayaType,
|
|
files: vm.uploadFiles,
|
|
notes: ""
|
|
};
|
|
try {
|
|
let res = await window.$gz.api.uploadAttachment(at);
|
|
|
|
if (res.error) {
|
|
window.$gz.errorHandler.handleFormError(res.error);
|
|
} else {
|
|
let ret = [];
|
|
for (let i = 0; i < res.data.length; i++) {
|
|
let o = res.data[i];
|
|
|
|
//let them attach any file type to the wiki since it supports it anyway
|
|
ret.push({
|
|
id: o.id,
|
|
url: window.$gz.api.attachmentDownloadUrl(o.id, o.contentType),
|
|
name: o.displayFileName
|
|
});
|
|
//}
|
|
}
|
|
//put into attachments list
|
|
vm.attachments = ret;
|
|
//NOW iterate upload files list and insert into wiki based on attachments
|
|
//insert into wiki
|
|
for (let i = 0; i < vm.uploadFiles.length; i++) {
|
|
let upFile = vm.uploadFiles[i];
|
|
for (let j = 0; j < vm.attachments.length; j++) {
|
|
let atFile = vm.attachments[j];
|
|
if (upFile.name == atFile.name) {
|
|
//found it
|
|
this.insertUrl(atFile.url, atFile.name);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
//finally, clear the upload files
|
|
vm.uploadFiles = [];
|
|
}
|
|
} catch (error) {
|
|
window.$gz.errorHandler.handleFormError(error);
|
|
}
|
|
},
|
|
onDrop(ev) {
|
|
//Drop image file
|
|
var files = Array.from(ev.dataTransfer.files);
|
|
if (files.length > 0) {
|
|
//handle file drop
|
|
var files = Array.from(ev.dataTransfer.files);
|
|
if (files.length > 0) {
|
|
this.uploadFiles = files;
|
|
this.upload();
|
|
}
|
|
//if an image then put it directly in viewable, if not an image then make a link and keep as an attach
|
|
} else {
|
|
//maybe an url?
|
|
let url = ev.dataTransfer.getData("text");
|
|
this.insertUrl(url);
|
|
}
|
|
},
|
|
insertUrl(url, name) {
|
|
if (url) {
|
|
let isImageUrl = false;
|
|
//attachment?
|
|
if (url.includes("attachment/download/")) {
|
|
//it's an attachment url so fixup accordingly
|
|
//i paramter added by gzapi::attachmentDownloadUrl function
|
|
isImageUrl = url.includes("&i=");
|
|
let m = url.match(/attachment\/download\/(.*)\?t=/);
|
|
if (m.length > 1) {
|
|
url = "[ATTACH:" + m[1] + "]";
|
|
} else {
|
|
url = null;
|
|
}
|
|
} else {
|
|
//External url, sniff out if it's an image
|
|
isImageUrl = window.$gz.util.isImageAttachment(url);
|
|
}
|
|
if (url != null) {
|
|
//insert into textarea
|
|
let txt = this.getSelectedText();
|
|
if (!txt) {
|
|
txt = name;
|
|
}
|
|
if (txt) {
|
|
if (isImageUrl) {
|
|
this.replaceSelectedText(
|
|
" \n' + txt + "\n" //keep original selected text otherwise it will vanish
|
|
);
|
|
} else {
|
|
//regular url not image
|
|
this.replaceSelectedText(
|
|
"[" + txt + "](" + url + ' "' + txt + '")\n'
|
|
);
|
|
}
|
|
} else {
|
|
//No selected text
|
|
if (isImageUrl) {
|
|
this.replaceSelectedText("\n");
|
|
} else {
|
|
//regular no text non image url
|
|
this.replaceSelectedText("<" + url + ">\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
//handle accordingly
|
|
}
|
|
}
|
|
|
|
//----------end methods----------
|
|
}
|
|
};
|
|
</script>
|