834 lines
27 KiB
Vue
834 lines
27 KiB
Vue
<template>
|
|
<div>
|
|
<template v-if="readOnly">
|
|
<div>
|
|
<v-btn depressed tile @click="switchView()">
|
|
Wiki<v-icon right>{{ switchViewIcon() }}</v-icon></v-btn
|
|
>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<span class="v-label v-label--active theme--light">
|
|
Wiki
|
|
</span>
|
|
<div class="mt-2">
|
|
<v-btn-toggle v-model="currentView">
|
|
<v-btn color="white" :value="view.HIDDEN_VIEW">
|
|
<v-icon>fa-eye-slash</v-icon>
|
|
</v-btn>
|
|
<v-btn color="white" :value="view.WIKI_VIEW">
|
|
<v-icon>fa-eye</v-icon>
|
|
</v-btn>
|
|
<v-btn color="white" :value="view.DESIGN_VIEW">
|
|
<v-icon>fa-edit</v-icon>
|
|
</v-btn>
|
|
<v-btn color="white" :value="view.SPLIT_VIEW">
|
|
<v-icon>fa-columns</v-icon>
|
|
</v-btn>
|
|
</v-btn-toggle>
|
|
</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>fa-bold</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('italic')">
|
|
<v-icon>fa-italic</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('strike')">
|
|
<v-icon>fa-strikethrough</v-icon></v-btn
|
|
>
|
|
|
|
<!-- HEADINGS -->
|
|
<v-menu offset-y>
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn depressed tile v-on="on">
|
|
<v-icon>fa-heading</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>fa-minus</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('quote')">
|
|
<v-icon>fa-quote-left</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('ul')">
|
|
<v-icon>fa-list-ul</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('ol')">
|
|
<v-icon>fa-list-ol</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('task')">
|
|
<v-icon>fa-check-square</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>fa-table</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="fa-arrows-alt-h"
|
|
></v-slider>
|
|
|
|
<v-slider
|
|
prepend-icon="fa-arrows-alt-v"
|
|
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>fa-link</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>fa-image</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
|
|
:label="$ay.t('Attachments')"
|
|
v-model="selectedImageAttachment"
|
|
:items="availableAttachments()"
|
|
item-text="name"
|
|
item-value="id"
|
|
></v-select>
|
|
<!--
|
|
<v-file-input
|
|
:label="$ay.t('AttachFile')"
|
|
prepend-icon="fa-paperclip"
|
|
></v-file-input> -->
|
|
|
|
<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>fa-code</v-icon></v-btn
|
|
>
|
|
<v-btn depressed tile @click="editClick('codeblock')">
|
|
<v-icon>fa-square-full</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
|
|
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="aywiki"
|
|
v-html="compiledOutput()"
|
|
></div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-sheet>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
/*
|
|
This code written in Mid April 2020 about 4 weeks into Covid-19 self isolation
|
|
It's just starting to look like BC has "flattened the curve", but other places are still a shit-show
|
|
*/
|
|
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,
|
|
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: "",
|
|
selectedImageAttachment: null
|
|
};
|
|
},
|
|
props: {
|
|
value: String,
|
|
readOnly: Boolean,
|
|
attachments: Array
|
|
},
|
|
watch: {
|
|
value(value) {
|
|
this.localVal = value;
|
|
}
|
|
},
|
|
methods: {
|
|
compiledOutput() {
|
|
if (this.localVal.length == 0) {
|
|
return "";
|
|
}
|
|
return DOMPurify.sanitize(marked(this.localVal, { breaks: true }));
|
|
},
|
|
onResize() {
|
|
// this.editAreaHeight = window.innerHeight / 2;
|
|
this.editAreaHeight = window.innerHeight * 0.8;
|
|
},
|
|
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
|
|
);
|
|
},
|
|
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
|
|
);
|
|
},
|
|
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 "fa-eye";
|
|
} else {
|
|
return "fa-eye-slash";
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (this.currentView) {
|
|
case this.view.HIDDEN_VIEW:
|
|
return "fa-eye";
|
|
case this.view.WIKI_VIEW:
|
|
return "fa-columns";
|
|
case this.view.SPLIT_VIEW:
|
|
return "fa-feather";
|
|
case this.view.DESIGN_VIEW:
|
|
return "fa-eye";
|
|
}
|
|
},
|
|
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 ? "fa-eye-slash" : "fa-eye";
|
|
},
|
|
//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();
|
|
// console.log("Selected block:[" + s + "]");
|
|
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";
|
|
// console.log("RET:[" + ret + "]");
|
|
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":
|
|
//")
|
|
{
|
|
if (ex == 1) {
|
|
throw "Attachment mode TODO";
|
|
//todo: ATTACHEDFILEMODE//attached file mode?
|
|
} else {
|
|
this.imageMenu = false;
|
|
let 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("");
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw 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;
|
|
});
|
|
},
|
|
availableAttachments() {
|
|
return [
|
|
{ id: 0, name: "Stub attachment one" },
|
|
{ id: 1, name: "Stub attachment two" },
|
|
{ id: 2, name: "Stub attachment three" }
|
|
];
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
|
|
|
|
|
|
|
|
todo: widget form, remove wiki menu option
|
|
|
|
todo: Add wikiContent field to form defintions at server so can hide or show in form customization
|
|
- Also for dataLists? (for reporting not grid I mean)
|
|
|
|
todo: event log type just for edit wiki?
|
|
- this is because a wiki is not a discrete object in v8 so rights follow object itself and maybe it's necessary to know when wiki was edited?
|
|
- or is this more important than some other events? Not sure.
|
|
|
|
Clean up the example markdown, go through and use mine and sprinkle in the marked sample stuff where it differs
|
|
- Make sure images ONLY come from our own server, not any other
|
|
- Maybe make a key image and put on our server for wiki example so we can if we feel like it track usage of demo data
|
|
- Put a block of emojis in it with a link to the help docs regarding emojis for extra coolness
|
|
|
|
todo: STYLE / OUTPUT CSS IMPROVEMENTS
|
|
- Check with MARKED to see what css they use or require or something, maybe I'm missing something they have on their site before I roll my own
|
|
- the TOAST one is pretty cool looking
|
|
- maybe whatever is used by my docs would work too
|
|
- also there are a lot on the web search for markdown style css
|
|
- CODE BLOCK: why is it indenting the start of a code block?
|
|
- TASK Style the task markdown output, it looks pretty bleak right now
|
|
I stole the css from toast it's in the notes, search for task-list-item
|
|
also maybe there's a cleaner way try a google search once you see how they did it
|
|
- TABLES currently look shitty, find a proper style for them with boxes and shit, maybe alternating background on rows etc
|
|
- STRIKETHROUGH hard to see
|
|
|
|
|
|
|
|
*/
|
|
</script>
|