aloha-vue 1.2.64 → 1.2.65-a2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "Vue.js"
15
15
  ],
16
16
  "homepage": "https://github.com/ilia-brykin/aloha/#README.md",
17
- "version": "1.2.64",
17
+ "version": "1.2.65-a2",
18
18
  "author": {
19
19
  "name": "Ilia Brykin",
20
20
  "email": "brykin.ilia@gmail.com"
@@ -0,0 +1,53 @@
1
+ import {
2
+ ref,
3
+ } from "vue";
4
+
5
+ export const tinymcePluginOptions = ref({
6
+ propsDefault: {
7
+ branding: true,
8
+ contentCustomStyle: undefined,
9
+ contentLangs: [
10
+ { title: "English", code: "en" },
11
+ { title: "Spanish", code: "es" },
12
+ { title: "French", code: "fr" },
13
+ { title: "Deutsch", code: "de" },
14
+ { title: "Portuguese", code: "pt" },
15
+ { title: "Chinese", code: "zh" },
16
+ ],
17
+ languageDefault: "de",
18
+ maxlength: undefined,
19
+ menu: undefined,
20
+ menubar: false,
21
+ plugins: "advlist code emoticons link lists table help example",
22
+ promotion: false,
23
+ rows: undefined,
24
+ toolbar: [
25
+ { name: "Formatierung", items: ["bold", "italic", "underline"] },
26
+ { name: "Ausrichtung", items: ["alignleft", "aligncenter", "alignright", "alignjustify"] },
27
+ { name: "Liste", items: ["bullist", "numlist"] },
28
+ { name: "Einrücken", items: ["indent", "outdent"] },
29
+ { name: "Sprache", items: ["language"] },
30
+ { name: "Link", items: ["link", "unlink"] },
31
+ { name: "Historie", items: ["undo", "redo"] },
32
+ { name: "Hilfe", items: ["help"] },
33
+ ],
34
+ toolbarMode: "wrap",
35
+ validElements: "@[style],a[href|target|title],strong/b[style],em/i[style],div[style],br[style],p[style],span[style],ul[style],ol[style],li[style],table[],thead[],tbody[],th[],tr[],td[]",
36
+ validStyles: {
37
+ "*": "text-align,padding-left,text-decoration",
38
+ ul: "list-style-type",
39
+ ol: "list-style-type",
40
+ },
41
+ },
42
+ });
43
+
44
+ export default {
45
+ install: (app, {
46
+ propsDefault = {},
47
+ } = {}) => {
48
+ tinymcePluginOptions.value.propsDefault = {
49
+ ...tinymcePluginOptions.value.propsDefault,
50
+ ...propsDefault,
51
+ };
52
+ },
53
+ };
@@ -15,6 +15,10 @@ import ATinymceAPI from "./compositionAPI/ATinymceAPI";
15
15
  import UiAPI from "../compositionApi/UiAPI";
16
16
  import UiStyleHideAPI from "../compositionApi/UiStyleHideAPI";
17
17
 
18
+ import {
19
+ tinymcePluginOptions,
20
+ } from "../../plugins/ATinymcePlugin";
21
+
18
22
  export default {
19
23
  name: "ATinymce",
20
24
  mixins: [
@@ -24,71 +28,62 @@ export default {
24
28
  branding: {
25
29
  type: Boolean,
26
30
  required: false,
27
- default: true,
31
+ default: () => tinymcePluginOptions.value.propsDefault.branding,
32
+ },
33
+ contentCustomStyle: {
34
+ type: String,
35
+ required: false,
36
+ default: () => tinymcePluginOptions.value.propsDefault.contentCustomStyle,
28
37
  },
29
38
  contentLangs: {
30
39
  type: Array,
31
40
  required: false,
32
- default: () => [
33
- { title: "English", code: "en" },
34
- { title: "Spanish", code: "es" },
35
- { title: "French", code: "fr" },
36
- { title: "Deutsch", code: "de" },
37
- { title: "Portuguese", code: "pt" },
38
- { title: "Chinese", code: "zh" },
39
- ],
41
+ default: () => tinymcePluginOptions.value.propsDefault.contentLangs,
40
42
  },
41
43
  languageDefault: {
42
44
  type: String,
43
45
  required: false,
44
- default: "de",
46
+ default: () => tinymcePluginOptions.value.propsDefault.languageDefault,
45
47
  },
46
48
  maxlength: {
47
49
  type: [String, Number],
48
50
  required: false,
51
+ default: () => tinymcePluginOptions.value.propsDefault.maxlength,
49
52
  },
50
53
  menu: {
51
54
  type: Object,
52
55
  required: false,
53
- default: undefined,
56
+ default: () => tinymcePluginOptions.value.propsDefault.menu,
54
57
  },
55
58
  menubar: {
56
59
  type: [String, Boolean],
57
60
  required: false,
58
- default: false,
61
+ default: () => tinymcePluginOptions.value.propsDefault.menubar,
59
62
  },
60
63
  plugins: {
61
64
  type: String,
62
65
  required: false,
63
- default: "advlist code emoticons link lists table help",
66
+ default: () => tinymcePluginOptions.value.propsDefault.plugins,
64
67
  },
65
68
  promotion: {
66
69
  type: Boolean,
67
70
  required: false,
68
- default: false,
71
+ default: () => tinymcePluginOptions.value.propsDefault.promotion,
69
72
  },
70
73
  rows: {
71
74
  type: [String, Number],
72
75
  required: false,
76
+ default: () => tinymcePluginOptions.value.propsDefault.rows,
73
77
  },
74
78
  toolbar: {
75
79
  type: [String, Object, Array],
76
80
  required: false,
77
- default: () => ([
78
- { name: "Formatierung", items: ["bold", "italic", "underline"] },
79
- { name: "Ausrichtung", items: ["alignleft", "aligncenter", "alignright", "alignjustify"] },
80
- { name: "Liste", items: ["bullist", "numlist"] },
81
- { name: "Einrücken", items: ["indent", "outdent"] },
82
- { name: "Sprache", items: ["language"] },
83
- { name: "Link", items: ["link", "unlink"] },
84
- { name: "Historie", items: ["undo", "redo"] },
85
- { name: "Hilfe", items: ["help"] },
86
- ]),
81
+ default: () => tinymcePluginOptions.value.propsDefault.toolbar,
87
82
  },
88
83
  toolbarMode: {
89
84
  type: String,
90
85
  required: false,
91
- default: "wrap",
86
+ default: () => tinymcePluginOptions.value.propsDefault.toolbarMode,
92
87
  validator: value => ["floating", "sliding", "scrolling", "wrap"].indexOf(value) !== -1,
93
88
  },
94
89
  type: {
@@ -96,6 +91,16 @@ export default {
96
91
  required: false,
97
92
  default: "tinymce",
98
93
  },
94
+ validElements: {
95
+ type: String,
96
+ required: false,
97
+ default: () => tinymcePluginOptions.value.propsDefault.validElements,
98
+ },
99
+ validStyles: {
100
+ type: Object,
101
+ required: false,
102
+ default: () => tinymcePluginOptions.value.propsDefault.validStyles,
103
+ },
99
104
  },
100
105
  setup(props, context) {
101
106
  const {
@@ -5,8 +5,11 @@ import {
5
5
  watch,
6
6
  } from "vue";
7
7
 
8
+ import MyPowerPaste from "../plugins/MyPowerPaste";
9
+
8
10
  /* Import TinyMCE */
9
11
  import tinymce from "tinymce";
12
+ tinymce.PluginManager.add("example", MyPowerPaste);
10
13
 
11
14
  /* Default icons are required. After that, import custom icons if applicable */
12
15
  import "tinymce/icons/default";
@@ -45,20 +48,27 @@ export default function ATinymceAPI(props, context, {
45
48
  htmlIdLocal = computed(() => ""),
46
49
  }) {
47
50
  const branding = toRef(props, "branding");
51
+ const contentCustomStyle = toRef(props, "contentCustomStyle");
48
52
  const contentLangs = toRef(props, "contentLangs");
49
53
  const disabled = toRef(props, "disabled");
50
54
  const languageDefault = toRef(props, "languageDefault");
51
55
  const menu = toRef(props, "menu");
52
56
  const menubar = toRef(props, "menubar");
57
+ const modelValue = toRef(props, "modelValue");
53
58
  const plugins = toRef(props, "plugins");
54
59
  const promotion = toRef(props, "promotion");
55
60
  const toolbar = toRef(props, "toolbar");
56
61
  const toolbarMode = toRef(props, "toolbarMode");
57
- const modelValue = toRef(props, "modelValue");
62
+ const validElements = toRef(props, "validElements");
63
+ const validStyles = toRef(props, "validStyles");
58
64
 
59
65
  let vueEditor = null;
60
66
  let modelValueLocal = undefined;
61
67
 
68
+ const contentStyle = computed(() => {
69
+ return `${ contentUiSkinCss.toString() }\n${ contentCss.toString() }\n${ contentCustomStyle.value ? contentCustomStyle.value : "" }`;
70
+ });
71
+
62
72
  const changeModelLocal = ({ model }) => {
63
73
  modelValueLocal = model;
64
74
  changeModel({ model });
@@ -74,7 +84,7 @@ export default function ATinymceAPI(props, context, {
74
84
  skin: false,
75
85
  content_css: false,
76
86
  entity_encoding: "raw",
77
- content_style: `${ contentUiSkinCss.toString() }\n${ contentCss.toString() }`,
87
+ content_style: contentStyle.value,
78
88
  branding: branding.value,
79
89
  promotion: promotion.value,
80
90
  language: languageDefault.value,
@@ -82,6 +92,9 @@ export default function ATinymceAPI(props, context, {
82
92
  menu: menu.value,
83
93
  menubar: menubar.value,
84
94
  readonly: !!disabled.value,
95
+ valid_elements: validElements.value,
96
+ valid_styles: validStyles.value,
97
+
85
98
  setup: editor => {
86
99
  vueEditor = editor;
87
100
  editor.on("change input undo redo", () => {
@@ -0,0 +1,204 @@
1
+ import {
2
+ forEach,
3
+ } from "lodash-es";
4
+
5
+ export default function(editor) {
6
+ // Event-Handler für Einfügevorgänge
7
+ editor.on("paste", function(e) {
8
+ const clipboardData = (e.clipboardData || window.clipboardData);
9
+ e.stopPropagation();
10
+ e.preventDefault();
11
+
12
+ const PAGE_HTML = clipboardData.getData("text/html");
13
+ const HTML_FRAGMENT = extractTextBetween({
14
+ html: PAGE_HTML,
15
+ start: "<!--StartFragment-->",
16
+ end: "<!--EndFragment-->",
17
+ });
18
+
19
+ if (isMsWord({ html: PAGE_HTML })) {
20
+ editor.insertContent(parseWord({ html: HTML_FRAGMENT }));
21
+ } else {
22
+ const cleanedHtml = HTML_FRAGMENT.replace(/\sdata-[a-z0-9-]+="[^"]*"/gi, "").replace(/\saria-[a-z0-9-]+="[^"]*"/gi, "");
23
+
24
+ editor.insertContent(cleanedHtml);
25
+ }
26
+ });
27
+ /* Return the metadata for the help plugin */
28
+ return {
29
+ getMetadata: () => ({
30
+ name: "My power paste",
31
+ url: "https://ilia-brykin.github.io/aloha/#/"
32
+ })
33
+ };
34
+ }
35
+
36
+ function extractTextBetween({ html, start, end }) {
37
+ const startIndex = html.indexOf(start);
38
+ const endIndex = html.indexOf(end);
39
+
40
+ if (startIndex !== -1 && endIndex !== -1) {
41
+ return html.substring(startIndex + start.length, endIndex).trim();
42
+ }
43
+ return html;
44
+ }
45
+
46
+ function isMsWord({ html }) {
47
+ const HTML_TAG = extractTextBetween({
48
+ html,
49
+ start: "<html",
50
+ end: ">",
51
+ });
52
+
53
+ return HTML_TAG.indexOf(":office:word") !== -1;
54
+ }
55
+
56
+ function parseWord({ html }) {
57
+ const parser = new DOMParser();
58
+ const doc = parser.parseFromString(html, "text/html");
59
+ const allChildren = doc.body.querySelectorAll(":scope > *");
60
+
61
+ const rootElement = document.createElement("div");
62
+ let currentList = null;
63
+ let lastLevel = 0;
64
+ let lastIndent = 0;
65
+ const listStack = [];
66
+
67
+ forEach(allChildren, element => {
68
+ const isTagP = element.tagName === "P";
69
+ if (!isTagP) {
70
+ rootElement.appendChild(element);
71
+ lastLevel = 0;
72
+ lastIndent = 0;
73
+ currentList = null;
74
+ return;
75
+ }
76
+
77
+ let htmlContent = element.innerHTML.trim();
78
+ const IS_NORMAL_PARAGRAPH = isNormalParagraph({ element, htmlContent });
79
+ htmlContent = htmlContent
80
+ .replace(/<u>/g, "<span style=\"text-decoration: underline;\">")
81
+ .replace(/<\/u>/g, "</span>")
82
+ .replace(/<!--\[if !supportLists]-->.*?<!--\[endif]-->/gs, "")
83
+ .replace(/&nbsp;/g, " ")
84
+ .replace(/^[\s·o§1-9]+[.)]?/g, "");
85
+
86
+ if (isTagP && IS_NORMAL_PARAGRAPH) {
87
+ const newParagraph = document.createElement("p");
88
+ newParagraph.innerHTML = htmlContent;
89
+ rootElement.appendChild(newParagraph);
90
+ lastLevel = 0;
91
+ lastIndent = 0;
92
+ currentList = null;
93
+ return;
94
+ }
95
+
96
+ const MARGIN_LEFT = parseInt(element.style.marginLeft) || 0;
97
+ const LIST_LEVEL = getCurrentLevelForList(element);
98
+ const { type, style } = getListTypeAndStyleFromSpanContent(element);
99
+
100
+ if (!currentList || LIST_LEVEL > lastLevel || MARGIN_LEFT > lastIndent) {
101
+ const newList = document.createElement(type);
102
+ if (style) {
103
+ newList.style.listStyleType = style;
104
+ }
105
+
106
+ if (currentList) {
107
+ currentList.appendChild(newList);
108
+ } else {
109
+ rootElement.appendChild(newList);
110
+ }
111
+ currentList = newList;
112
+ listStack.push({ element: newList, level: LIST_LEVEL, marginLeft: MARGIN_LEFT });
113
+ } else if (LIST_LEVEL < lastLevel) {
114
+ while (listStack.length > 1 && LIST_LEVEL < listStack[listStack.length - 1].level) {
115
+ listStack.pop();
116
+ currentList = listStack[listStack.length - 1].element;
117
+ }
118
+ } else if (MARGIN_LEFT < lastIndent && listStack.length > 1) {
119
+ while (listStack.length > 1 && MARGIN_LEFT < listStack[listStack.length - 1].marginLeft) {
120
+ listStack.pop();
121
+ currentList = listStack[listStack.length - 1].element;
122
+ }
123
+ }
124
+
125
+ const li = document.createElement("li");
126
+ li.innerHTML = htmlContent;
127
+ currentList.appendChild(li);
128
+
129
+ lastLevel = LIST_LEVEL;
130
+ lastIndent = MARGIN_LEFT;
131
+ });
132
+
133
+ return rootElement.innerHTML;
134
+ }
135
+
136
+ function getListTypeAndStyleFromSpanContent(p) {
137
+ const spans = p.querySelectorAll("span");
138
+ let spanContent = "";
139
+ forEach(spans, span => {
140
+ const styleAttr = span.getAttribute("style");
141
+ if (styleAttr && styleAttr.includes("mso-list:Ignore")) {
142
+ spanContent = span.textContent || "";
143
+ return false;
144
+ }
145
+ });
146
+
147
+ spanContent = spanContent.trim();
148
+ if (!spanContent) {
149
+ return { type: "ul", style: "" };
150
+ }
151
+ // Check for ordered list with Arabic numerals or numbers within parentheses
152
+ if (/^\d+[.)]/.test(spanContent) || /^\(\d+\)/.test(spanContent)) {
153
+ return { type: "ol", style: "" }; // Default style for <ol>
154
+ }
155
+ // Check for ordered list with uppercase Roman numerals
156
+ if (/^[IVXLCDM]+\./.test(spanContent)) {
157
+ return { type: "ol", style: "upper-roman" };
158
+ }
159
+ // Check for ordered list with lowercase Roman numerals
160
+ if (/^[ivxlcdm]+\./.test(spanContent)) {
161
+ return { type: "ol", style: "lower-roman" };
162
+ }
163
+ // Check for ordered list with uppercase letters
164
+ if (/^[A-Z]+[.)]/.test(spanContent)) {
165
+ return { type: "ol", style: "upper-alpha" };
166
+ }
167
+ // Check for ordered list with lowercase letters
168
+ if (/^[a-z]+[.)]/.test(spanContent)) {
169
+ return { type: "ol", style: "lower-alpha" };
170
+ }
171
+ // Check for unordered list with various symbols
172
+ if (/^[·o§]/.test(spanContent)) {
173
+ // Determine list style based on the symbol
174
+ let style;
175
+ switch (spanContent.trim()[0]) {
176
+ case "·": style = "disc"; break;
177
+ case "o": style = "circle"; break;
178
+ case "§": style = "square"; break;
179
+ default: style = "";
180
+ }
181
+ return { type: "ul", style: style };
182
+ }
183
+
184
+ return { type: "ul", style: "" };
185
+ }
186
+
187
+ function getCurrentLevelForList(p) {
188
+ const styleAttr = p.getAttribute("style");
189
+ let level = 1;
190
+ if (styleAttr) {
191
+ const levelMatch = styleAttr.match(/mso-list:l\d+ level(\d+)/);
192
+ if (levelMatch) {
193
+ level = parseInt(levelMatch[1], 10);
194
+ }
195
+ }
196
+
197
+ return level;
198
+ }
199
+
200
+ function isNormalParagraph({ element, htmlContent }) {
201
+ return element.className.includes("MsoNormal") ||
202
+ (!element.className.includes("MsoListParagraph") &&
203
+ !/mso-list:/.test(htmlContent));
204
+ }