@hostlink/nuxt-light 1.69.0 → 1.70.1
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/dist/module.json +1 -1
- package/dist/runtime/components/L/App.d.vue.ts +24 -1
- package/dist/runtime/components/L/App.vue +16 -1
- package/dist/runtime/components/L/App.vue.d.ts +24 -1
- package/dist/runtime/components/L/AppMain.d.vue.ts +33 -7
- package/dist/runtime/components/L/AppMain.vue +53 -0
- package/dist/runtime/components/L/AppMain.vue.d.ts +33 -7
- package/dist/runtime/components/L/DialogInsertEditImage.d.vue.ts +20 -0
- package/dist/runtime/components/L/DialogInsertEditImage.vue +133 -0
- package/dist/runtime/components/L/DialogInsertEditImage.vue.d.ts +20 -0
- package/dist/runtime/components/L/DialogInsertEditLink.d.vue.ts +20 -0
- package/dist/runtime/components/L/DialogInsertEditLink.vue +64 -0
- package/dist/runtime/components/L/DialogInsertEditLink.vue.d.ts +20 -0
- package/dist/runtime/components/L/DialogSelectFileFromFileManager.d.vue.ts +9 -0
- package/dist/runtime/components/L/DialogSelectFileFromFileManager.vue +25 -0
- package/dist/runtime/components/L/DialogSelectFileFromFileManager.vue.d.ts +9 -0
- package/dist/runtime/components/L/Editor.vue +237 -15
- package/dist/runtime/components/L/FileManager.vue +1 -1
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -4,8 +4,31 @@ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
|
4
4
|
type __VLS_WithSlots<T, S> = T & (new () => {
|
|
5
5
|
$slots: S;
|
|
6
6
|
});
|
|
7
|
-
declare const __VLS_base: import("vue").DefineComponent<
|
|
7
|
+
declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
8
|
+
userMenuItems: {
|
|
9
|
+
type: ArrayConstructor;
|
|
10
|
+
default: () => never[];
|
|
11
|
+
};
|
|
12
|
+
headerButtons: {
|
|
13
|
+
type: ArrayConstructor;
|
|
14
|
+
default: () => never[];
|
|
15
|
+
};
|
|
16
|
+
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
17
|
+
userMenuItems: {
|
|
18
|
+
type: ArrayConstructor;
|
|
19
|
+
default: () => never[];
|
|
20
|
+
};
|
|
21
|
+
headerButtons: {
|
|
22
|
+
type: ArrayConstructor;
|
|
23
|
+
default: () => never[];
|
|
24
|
+
};
|
|
25
|
+
}>> & Readonly<{}>, {
|
|
26
|
+
userMenuItems: unknown[];
|
|
27
|
+
headerButtons: unknown[];
|
|
28
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
8
29
|
type __VLS_Slots = {
|
|
30
|
+
[x: `header-button-menu-${any}`]: ((props: {}) => any) | undefined;
|
|
31
|
+
} & {
|
|
9
32
|
header?: ((props: {}) => any) | undefined;
|
|
10
33
|
} & {
|
|
11
34
|
'user-menu'?: ((props: {}) => any) | undefined;
|
|
@@ -4,6 +4,16 @@ import { useLight, watch } from "#imports";
|
|
|
4
4
|
import { useQuasar } from "quasar";
|
|
5
5
|
import { q } from "#imports";
|
|
6
6
|
import { useRoute } from "vue-router";
|
|
7
|
+
defineProps({
|
|
8
|
+
userMenuItems: {
|
|
9
|
+
type: Array,
|
|
10
|
+
default: () => []
|
|
11
|
+
},
|
|
12
|
+
headerButtons: {
|
|
13
|
+
type: Array,
|
|
14
|
+
default: () => []
|
|
15
|
+
}
|
|
16
|
+
});
|
|
7
17
|
const route = useRoute();
|
|
8
18
|
const light = useLight();
|
|
9
19
|
const quasar = useQuasar();
|
|
@@ -83,7 +93,8 @@ if (app.value.facebookAppId) {
|
|
|
83
93
|
</q-page-container>
|
|
84
94
|
</q-layout>
|
|
85
95
|
|
|
86
|
-
<l-app-main v-else @logout="app.logged = false" v-bind="app"
|
|
96
|
+
<l-app-main v-else @logout="app.logged = false" v-bind="app" :user-menu-items="userMenuItems"
|
|
97
|
+
:header-buttons="headerButtons">
|
|
87
98
|
<template #header>
|
|
88
99
|
<slot name="header"></slot>
|
|
89
100
|
</template>
|
|
@@ -92,6 +103,10 @@ if (app.value.facebookAppId) {
|
|
|
92
103
|
<slot name="user-menu"></slot>
|
|
93
104
|
</template>
|
|
94
105
|
|
|
106
|
+
<template v-for="button in headerButtons" :key="button.name" #[`header-button-menu-${button.name}`]>
|
|
107
|
+
<slot :name="`header-button-menu-${button.name}`"></slot>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
95
110
|
<template #page-top>
|
|
96
111
|
<slot name="page-top"></slot>
|
|
97
112
|
</template>
|
|
@@ -4,8 +4,31 @@ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
|
4
4
|
type __VLS_WithSlots<T, S> = T & (new () => {
|
|
5
5
|
$slots: S;
|
|
6
6
|
});
|
|
7
|
-
declare const __VLS_base: import("vue").DefineComponent<
|
|
7
|
+
declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
8
|
+
userMenuItems: {
|
|
9
|
+
type: ArrayConstructor;
|
|
10
|
+
default: () => never[];
|
|
11
|
+
};
|
|
12
|
+
headerButtons: {
|
|
13
|
+
type: ArrayConstructor;
|
|
14
|
+
default: () => never[];
|
|
15
|
+
};
|
|
16
|
+
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
17
|
+
userMenuItems: {
|
|
18
|
+
type: ArrayConstructor;
|
|
19
|
+
default: () => never[];
|
|
20
|
+
};
|
|
21
|
+
headerButtons: {
|
|
22
|
+
type: ArrayConstructor;
|
|
23
|
+
default: () => never[];
|
|
24
|
+
};
|
|
25
|
+
}>> & Readonly<{}>, {
|
|
26
|
+
userMenuItems: unknown[];
|
|
27
|
+
headerButtons: unknown[];
|
|
28
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
8
29
|
type __VLS_Slots = {
|
|
30
|
+
[x: `header-button-menu-${any}`]: ((props: {}) => any) | undefined;
|
|
31
|
+
} & {
|
|
9
32
|
header?: ((props: {}) => any) | undefined;
|
|
10
33
|
} & {
|
|
11
34
|
'user-menu'?: ((props: {}) => any) | undefined;
|
|
@@ -1,18 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
interface UserMenuItem {
|
|
2
|
+
label: string;
|
|
3
|
+
icon?: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
click?: () => void;
|
|
6
|
+
visible?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface HeaderButton {
|
|
9
|
+
name: string;
|
|
10
|
+
icon: string;
|
|
11
|
+
tooltip?: string;
|
|
12
|
+
to?: string;
|
|
13
|
+
click?: () => void;
|
|
14
|
+
menu?: boolean;
|
|
15
|
+
visible?: boolean;
|
|
16
|
+
desktop?: boolean;
|
|
17
|
+
mobile?: boolean;
|
|
18
|
+
}
|
|
19
|
+
type __VLS_Props = {
|
|
20
|
+
userMenuItems?: UserMenuItem[];
|
|
21
|
+
headerButtons?: HeaderButton[];
|
|
22
|
+
};
|
|
23
|
+
declare var __VLS_38: {}, __VLS_61: `header-button-menu-${string}`, __VLS_62: {}, __VLS_132: `header-button-menu-${string}`, __VLS_133: {}, __VLS_340: {}, __VLS_482: {}, __VLS_489: {};
|
|
2
24
|
type __VLS_Slots = {} & {
|
|
25
|
+
[K in NonNullable<typeof __VLS_61>]?: (props: typeof __VLS_62) => any;
|
|
26
|
+
} & {
|
|
27
|
+
[K in NonNullable<typeof __VLS_132>]?: (props: typeof __VLS_133) => any;
|
|
28
|
+
} & {
|
|
3
29
|
header?: (props: typeof __VLS_38) => any;
|
|
4
30
|
} & {
|
|
5
|
-
'user-menu'?: (props: typeof
|
|
31
|
+
'user-menu'?: (props: typeof __VLS_340) => any;
|
|
6
32
|
} & {
|
|
7
|
-
'page-top'?: (props: typeof
|
|
33
|
+
'page-top'?: (props: typeof __VLS_482) => any;
|
|
8
34
|
} & {
|
|
9
|
-
'page-bottom'?: (props: typeof
|
|
35
|
+
'page-bottom'?: (props: typeof __VLS_489) => any;
|
|
10
36
|
};
|
|
11
|
-
declare const __VLS_base: import("vue").DefineComponent<
|
|
37
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
38
|
logout: (...args: any[]) => void;
|
|
13
|
-
}, string, import("vue").PublicProps, Readonly<
|
|
39
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
40
|
onLogout?: ((...args: any[]) => any) | undefined;
|
|
15
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions,
|
|
41
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
42
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
17
43
|
declare const _default: typeof __VLS_export;
|
|
18
44
|
export default _default;
|
|
@@ -7,6 +7,10 @@ import { ref, computed, reactive, provide, watch, toRaw, onMounted } from "vue";
|
|
|
7
7
|
import { useRuntimeConfig } from "nuxt/app";
|
|
8
8
|
import { logout } from "@hostlink/light";
|
|
9
9
|
const emits = defineEmits(["logout"]);
|
|
10
|
+
defineProps({
|
|
11
|
+
userMenuItems: { type: Array, required: false },
|
|
12
|
+
headerButtons: { type: Array, required: false }
|
|
13
|
+
});
|
|
10
14
|
const $q = useQuasar();
|
|
11
15
|
$q.loading.show();
|
|
12
16
|
const config = useRuntimeConfig();
|
|
@@ -255,6 +259,45 @@ const onLogout = async () => {
|
|
|
255
259
|
|
|
256
260
|
<slot name="header"></slot>
|
|
257
261
|
|
|
262
|
+
<!-- Header buttons (desktop) -->
|
|
263
|
+
<template v-for="button in headerButtons || []" :key="button.name">
|
|
264
|
+
<q-btn v-if="button.visible !== false && button.desktop !== false" round flat dense
|
|
265
|
+
:icon="button.icon" class="q-mr-sm gt-xs" @click="button.click"
|
|
266
|
+
:to="button.to ? button.to : void 0">
|
|
267
|
+
<q-tooltip v-if="button.tooltip">{{ $t(button.tooltip) }}</q-tooltip>
|
|
268
|
+
<q-menu v-if="button.menu">
|
|
269
|
+
<slot :name="`header-button-menu-${button.name}`"></slot>
|
|
270
|
+
</q-menu>
|
|
271
|
+
</q-btn>
|
|
272
|
+
</template>
|
|
273
|
+
|
|
274
|
+
<!-- Header buttons (mobile hamburger) -->
|
|
275
|
+
<q-btn round flat dense icon="menu" class="q-mr-sm lt-sm" v-if="headerButtons?.length">
|
|
276
|
+
<q-tooltip>More</q-tooltip>
|
|
277
|
+
<q-menu>
|
|
278
|
+
<q-list>
|
|
279
|
+
<template v-for="button in headerButtons || []" :key="button.name">
|
|
280
|
+
<q-item v-if="button.visible !== false && button.mobile !== false"
|
|
281
|
+
:clickable="!button.menu && (!!button.to || !!button.click)" @click="button.click"
|
|
282
|
+
:to="button.to ? button.to : void 0">
|
|
283
|
+
<q-item-section avatar>
|
|
284
|
+
<q-icon :name="button.icon" />
|
|
285
|
+
</q-item-section>
|
|
286
|
+
<q-item-section>{{ button.tooltip ? $t(button.tooltip) : button.name
|
|
287
|
+
}}</q-item-section>
|
|
288
|
+
<q-item-section side v-if="button.menu">
|
|
289
|
+
<q-btn flat dense icon="arrow_right">
|
|
290
|
+
<q-menu anchor="top end" self="top start">
|
|
291
|
+
<slot :name="`header-button-menu-${button.name}`"></slot>
|
|
292
|
+
</q-menu>
|
|
293
|
+
</q-btn>
|
|
294
|
+
</q-item-section>
|
|
295
|
+
</q-item>
|
|
296
|
+
</template>
|
|
297
|
+
</q-list>
|
|
298
|
+
</q-menu>
|
|
299
|
+
</q-btn>
|
|
300
|
+
|
|
258
301
|
<q-btn :icon="isFav ? 'favorite' : 'sym_o_favorite'" round flat dense class="q-mr-xs"
|
|
259
302
|
@click="onToggleFav" v-if="app.hasFavorite">
|
|
260
303
|
</q-btn>
|
|
@@ -337,6 +380,16 @@ const onLogout = async () => {
|
|
|
337
380
|
|
|
338
381
|
<slot name="user-menu"></slot>
|
|
339
382
|
|
|
383
|
+
<q-item v-for="item in userMenuItems || []" :key="item.label" v-close-popup :to="item.to"
|
|
384
|
+
clickable @click="item.click" v-show="item.visible !== false">
|
|
385
|
+
<q-item-section avatar v-if="item.icon">
|
|
386
|
+
<q-icon :name="item.icon" />
|
|
387
|
+
</q-item-section>
|
|
388
|
+
<q-item-section>
|
|
389
|
+
<q-item-label>{{ $t(item.label) }}</q-item-label>
|
|
390
|
+
</q-item-section>
|
|
391
|
+
</q-item>
|
|
392
|
+
|
|
340
393
|
<q-separator />
|
|
341
394
|
|
|
342
395
|
<q-item @click="onLogout" clickable>
|
|
@@ -1,18 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
interface UserMenuItem {
|
|
2
|
+
label: string;
|
|
3
|
+
icon?: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
click?: () => void;
|
|
6
|
+
visible?: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface HeaderButton {
|
|
9
|
+
name: string;
|
|
10
|
+
icon: string;
|
|
11
|
+
tooltip?: string;
|
|
12
|
+
to?: string;
|
|
13
|
+
click?: () => void;
|
|
14
|
+
menu?: boolean;
|
|
15
|
+
visible?: boolean;
|
|
16
|
+
desktop?: boolean;
|
|
17
|
+
mobile?: boolean;
|
|
18
|
+
}
|
|
19
|
+
type __VLS_Props = {
|
|
20
|
+
userMenuItems?: UserMenuItem[];
|
|
21
|
+
headerButtons?: HeaderButton[];
|
|
22
|
+
};
|
|
23
|
+
declare var __VLS_38: {}, __VLS_61: `header-button-menu-${string}`, __VLS_62: {}, __VLS_132: `header-button-menu-${string}`, __VLS_133: {}, __VLS_340: {}, __VLS_482: {}, __VLS_489: {};
|
|
2
24
|
type __VLS_Slots = {} & {
|
|
25
|
+
[K in NonNullable<typeof __VLS_61>]?: (props: typeof __VLS_62) => any;
|
|
26
|
+
} & {
|
|
27
|
+
[K in NonNullable<typeof __VLS_132>]?: (props: typeof __VLS_133) => any;
|
|
28
|
+
} & {
|
|
3
29
|
header?: (props: typeof __VLS_38) => any;
|
|
4
30
|
} & {
|
|
5
|
-
'user-menu'?: (props: typeof
|
|
31
|
+
'user-menu'?: (props: typeof __VLS_340) => any;
|
|
6
32
|
} & {
|
|
7
|
-
'page-top'?: (props: typeof
|
|
33
|
+
'page-top'?: (props: typeof __VLS_482) => any;
|
|
8
34
|
} & {
|
|
9
|
-
'page-bottom'?: (props: typeof
|
|
35
|
+
'page-bottom'?: (props: typeof __VLS_489) => any;
|
|
10
36
|
};
|
|
11
|
-
declare const __VLS_base: import("vue").DefineComponent<
|
|
37
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
38
|
logout: (...args: any[]) => void;
|
|
13
|
-
}, string, import("vue").PublicProps, Readonly<
|
|
39
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
14
40
|
onLogout?: ((...args: any[]) => any) | undefined;
|
|
15
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions,
|
|
41
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
42
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
17
43
|
declare const _default: typeof __VLS_export;
|
|
18
44
|
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface DialogInsertEditImageProps {
|
|
2
|
+
initialSource?: string;
|
|
3
|
+
initialAlt?: string;
|
|
4
|
+
initialWidth?: number | null;
|
|
5
|
+
initialHeight?: number | null;
|
|
6
|
+
}
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditImageProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
+
hide: (...args: any[]) => void;
|
|
9
|
+
ok: (...args: any[]) => void;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<DialogInsertEditImageProps> & Readonly<{
|
|
11
|
+
onHide?: ((...args: any[]) => any) | undefined;
|
|
12
|
+
onOk?: ((...args: any[]) => any) | undefined;
|
|
13
|
+
}>, {
|
|
14
|
+
initialSource: string;
|
|
15
|
+
initialAlt: string;
|
|
16
|
+
initialWidth: number | null;
|
|
17
|
+
initialHeight: number | null;
|
|
18
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
3
|
+
import { useDialogPluginComponent } from "quasar";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
initialSource: { type: String, required: false, default: "" },
|
|
6
|
+
initialAlt: { type: String, required: false, default: "" },
|
|
7
|
+
initialWidth: { type: [Number, null], required: false, default: null },
|
|
8
|
+
initialHeight: { type: [Number, null], required: false, default: null }
|
|
9
|
+
});
|
|
10
|
+
defineEmits([
|
|
11
|
+
...useDialogPluginComponent.emits
|
|
12
|
+
]);
|
|
13
|
+
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
|
14
|
+
const source = ref(props.initialSource);
|
|
15
|
+
const alt = ref(props.initialAlt);
|
|
16
|
+
const width = ref(props.initialWidth);
|
|
17
|
+
const height = ref(props.initialHeight);
|
|
18
|
+
const lockAspectRatio = ref(true);
|
|
19
|
+
const aspectRatio = ref(null);
|
|
20
|
+
const canSubmit = computed(() => source.value.trim().length > 0);
|
|
21
|
+
const toPositiveNumber = (value) => {
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
24
|
+
};
|
|
25
|
+
const updateImageMeta = (src) => {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
if (!src) {
|
|
28
|
+
aspectRatio.value = null;
|
|
29
|
+
resolve();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const img = new Image();
|
|
33
|
+
img.onload = () => {
|
|
34
|
+
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
|
|
35
|
+
aspectRatio.value = img.naturalWidth / img.naturalHeight;
|
|
36
|
+
if (!width.value) width.value = img.naturalWidth;
|
|
37
|
+
if (!height.value) height.value = img.naturalHeight;
|
|
38
|
+
}
|
|
39
|
+
resolve();
|
|
40
|
+
};
|
|
41
|
+
img.onerror = () => resolve();
|
|
42
|
+
img.src = src;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
watch(() => source.value, (value) => {
|
|
46
|
+
updateImageMeta(value.trim());
|
|
47
|
+
}, { immediate: true });
|
|
48
|
+
const onWidthInput = (val) => {
|
|
49
|
+
const newWidth = toPositiveNumber(val);
|
|
50
|
+
width.value = newWidth;
|
|
51
|
+
if (lockAspectRatio.value && aspectRatio.value && newWidth) {
|
|
52
|
+
height.value = Math.round(newWidth / aspectRatio.value);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const onHeightInput = (val) => {
|
|
56
|
+
const newHeight = toPositiveNumber(val);
|
|
57
|
+
height.value = newHeight;
|
|
58
|
+
if (lockAspectRatio.value && aspectRatio.value && newHeight) {
|
|
59
|
+
width.value = Math.round(newHeight * aspectRatio.value);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const toggleLock = () => {
|
|
63
|
+
lockAspectRatio.value = !lockAspectRatio.value;
|
|
64
|
+
if (lockAspectRatio.value && aspectRatio.value && width.value) {
|
|
65
|
+
height.value = Math.round(width.value / aspectRatio.value);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const onConfirm = () => {
|
|
69
|
+
if (!canSubmit.value) return;
|
|
70
|
+
onDialogOK({
|
|
71
|
+
source: source.value.trim(),
|
|
72
|
+
alt: alt.value.trim(),
|
|
73
|
+
width: width.value,
|
|
74
|
+
height: height.value
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
81
|
+
<q-card style="min-width: 560px; max-width: 90vw;">
|
|
82
|
+
<q-card-section class="text-h6">
|
|
83
|
+
Insert/Edit Image
|
|
84
|
+
</q-card-section>
|
|
85
|
+
|
|
86
|
+
<q-card-section class="q-gutter-md">
|
|
87
|
+
<q-input v-model="source" label="Source" outlined dense />
|
|
88
|
+
<q-input v-model="alt" label="Alternative Description" outlined dense />
|
|
89
|
+
|
|
90
|
+
<div class="row items-center q-col-gutter-sm">
|
|
91
|
+
<div class="col">
|
|
92
|
+
<q-input
|
|
93
|
+
:model-value="width"
|
|
94
|
+
@update:model-value="onWidthInput"
|
|
95
|
+
type="number"
|
|
96
|
+
min="1"
|
|
97
|
+
label="Width"
|
|
98
|
+
outlined
|
|
99
|
+
dense
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="col-auto">
|
|
103
|
+
<q-btn
|
|
104
|
+
flat
|
|
105
|
+
round
|
|
106
|
+
:icon="lockAspectRatio ? 'sym_o_lock' : 'sym_o_lock_open'"
|
|
107
|
+
:color="lockAspectRatio ? 'primary' : 'grey'"
|
|
108
|
+
@click="toggleLock"
|
|
109
|
+
>
|
|
110
|
+
<q-tooltip>Lock Aspect Ratio</q-tooltip>
|
|
111
|
+
</q-btn>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="col">
|
|
114
|
+
<q-input
|
|
115
|
+
:model-value="height"
|
|
116
|
+
@update:model-value="onHeightInput"
|
|
117
|
+
type="number"
|
|
118
|
+
min="1"
|
|
119
|
+
label="Height"
|
|
120
|
+
outlined
|
|
121
|
+
dense
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</q-card-section>
|
|
126
|
+
|
|
127
|
+
<q-card-actions align="right">
|
|
128
|
+
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
|
129
|
+
<q-btn color="primary" label="Insert" :disable="!canSubmit" @click="onConfirm" />
|
|
130
|
+
</q-card-actions>
|
|
131
|
+
</q-card>
|
|
132
|
+
</q-dialog>
|
|
133
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface DialogInsertEditImageProps {
|
|
2
|
+
initialSource?: string;
|
|
3
|
+
initialAlt?: string;
|
|
4
|
+
initialWidth?: number | null;
|
|
5
|
+
initialHeight?: number | null;
|
|
6
|
+
}
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditImageProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
+
hide: (...args: any[]) => void;
|
|
9
|
+
ok: (...args: any[]) => void;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<DialogInsertEditImageProps> & Readonly<{
|
|
11
|
+
onHide?: ((...args: any[]) => any) | undefined;
|
|
12
|
+
onOk?: ((...args: any[]) => any) | undefined;
|
|
13
|
+
}>, {
|
|
14
|
+
initialSource: string;
|
|
15
|
+
initialAlt: string;
|
|
16
|
+
initialWidth: number | null;
|
|
17
|
+
initialHeight: number | null;
|
|
18
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface DialogInsertEditLinkProps {
|
|
2
|
+
initialUrl?: string;
|
|
3
|
+
initialText?: string;
|
|
4
|
+
initialTitle?: string;
|
|
5
|
+
initialTarget?: '_self' | '_blank';
|
|
6
|
+
}
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditLinkProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
+
hide: (...args: any[]) => void;
|
|
9
|
+
ok: (...args: any[]) => void;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<DialogInsertEditLinkProps> & Readonly<{
|
|
11
|
+
onHide?: ((...args: any[]) => any) | undefined;
|
|
12
|
+
onOk?: ((...args: any[]) => any) | undefined;
|
|
13
|
+
}>, {
|
|
14
|
+
initialUrl: string;
|
|
15
|
+
initialText: string;
|
|
16
|
+
initialTitle: string;
|
|
17
|
+
initialTarget: "_self" | "_blank";
|
|
18
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { useDialogPluginComponent } from "quasar";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
initialUrl: { type: String, required: false, default: "" },
|
|
6
|
+
initialText: { type: String, required: false, default: "" },
|
|
7
|
+
initialTitle: { type: String, required: false, default: "" },
|
|
8
|
+
initialTarget: { type: String, required: false, default: "_self" }
|
|
9
|
+
});
|
|
10
|
+
defineEmits([
|
|
11
|
+
...useDialogPluginComponent.emits
|
|
12
|
+
]);
|
|
13
|
+
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
|
14
|
+
const url = ref(props.initialUrl);
|
|
15
|
+
const text = ref(props.initialText);
|
|
16
|
+
const title = ref(props.initialTitle);
|
|
17
|
+
const target = ref(props.initialTarget);
|
|
18
|
+
const targetOptions = [
|
|
19
|
+
{ label: "Current window", value: "_self" },
|
|
20
|
+
{ label: "New window", value: "_blank" }
|
|
21
|
+
];
|
|
22
|
+
const canSubmit = computed(() => url.value.trim().length > 0);
|
|
23
|
+
const onConfirm = () => {
|
|
24
|
+
if (!canSubmit.value) return;
|
|
25
|
+
onDialogOK({
|
|
26
|
+
url: url.value.trim(),
|
|
27
|
+
text: text.value.trim(),
|
|
28
|
+
title: title.value.trim(),
|
|
29
|
+
target: target.value
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
36
|
+
<q-card style="min-width: 520px; max-width: 90vw;">
|
|
37
|
+
<q-card-section class="text-h6">
|
|
38
|
+
Insert/Edit Link
|
|
39
|
+
</q-card-section>
|
|
40
|
+
|
|
41
|
+
<q-card-section class="q-gutter-md">
|
|
42
|
+
<q-input v-model="url" label="URL" outlined dense />
|
|
43
|
+
<q-input v-model="text" label="Text to display" outlined dense />
|
|
44
|
+
<q-input v-model="title" label="Title" outlined dense />
|
|
45
|
+
<q-select
|
|
46
|
+
v-model="target"
|
|
47
|
+
label="Open link in..."
|
|
48
|
+
:options="targetOptions"
|
|
49
|
+
option-label="label"
|
|
50
|
+
option-value="value"
|
|
51
|
+
emit-value
|
|
52
|
+
map-options
|
|
53
|
+
outlined
|
|
54
|
+
dense
|
|
55
|
+
/>
|
|
56
|
+
</q-card-section>
|
|
57
|
+
|
|
58
|
+
<q-card-actions align="right">
|
|
59
|
+
<q-btn flat label="Cancel" @click="onDialogCancel" />
|
|
60
|
+
<q-btn color="primary" label="Insert" :disable="!canSubmit" @click="onConfirm" />
|
|
61
|
+
</q-card-actions>
|
|
62
|
+
</q-card>
|
|
63
|
+
</q-dialog>
|
|
64
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface DialogInsertEditLinkProps {
|
|
2
|
+
initialUrl?: string;
|
|
3
|
+
initialText?: string;
|
|
4
|
+
initialTitle?: string;
|
|
5
|
+
initialTarget?: '_self' | '_blank';
|
|
6
|
+
}
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<DialogInsertEditLinkProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
+
hide: (...args: any[]) => void;
|
|
9
|
+
ok: (...args: any[]) => void;
|
|
10
|
+
}, string, import("vue").PublicProps, Readonly<DialogInsertEditLinkProps> & Readonly<{
|
|
11
|
+
onHide?: ((...args: any[]) => any) | undefined;
|
|
12
|
+
onOk?: ((...args: any[]) => any) | undefined;
|
|
13
|
+
}>, {
|
|
14
|
+
initialUrl: string;
|
|
15
|
+
initialText: string;
|
|
16
|
+
initialTitle: string;
|
|
17
|
+
initialTarget: "_self" | "_blank";
|
|
18
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
2
|
+
hide: (...args: any[]) => void;
|
|
3
|
+
ok: (...args: any[]) => void;
|
|
4
|
+
}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
|
|
5
|
+
onHide?: ((...args: any[]) => any) | undefined;
|
|
6
|
+
onOk?: ((...args: any[]) => any) | undefined;
|
|
7
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useDialogPluginComponent } from "quasar";
|
|
3
|
+
defineEmits([
|
|
4
|
+
...useDialogPluginComponent.emits
|
|
5
|
+
]);
|
|
6
|
+
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
|
|
7
|
+
const onInput = (value, row) => {
|
|
8
|
+
onDialogOK({ path: value, row });
|
|
9
|
+
};
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<q-dialog ref="dialogRef" @hide="onDialogHide" full-height full-width>
|
|
14
|
+
<q-card style="width: 100%; height: 100%;">
|
|
15
|
+
<Suspense>
|
|
16
|
+
<l-file-manager closable @close="onDialogCancel" @input="onInput"></l-file-manager>
|
|
17
|
+
<template #fallback>
|
|
18
|
+
<div class="flex flex-center" style="height: 100%;">
|
|
19
|
+
<q-spinner color="primary" size="3em" />
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
</Suspense>
|
|
23
|
+
</q-card>
|
|
24
|
+
</q-dialog>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
2
|
+
hide: (...args: any[]) => void;
|
|
3
|
+
ok: (...args: any[]) => void;
|
|
4
|
+
}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
|
|
5
|
+
onHide?: ((...args: any[]) => any) | undefined;
|
|
6
|
+
onOk?: ((...args: any[]) => any) | undefined;
|
|
7
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, ref, useAttrs } from "vue";
|
|
2
|
+
import { computed, defineAsyncComponent, ref, useAttrs } from "vue";
|
|
3
3
|
import { useQuasar } from "quasar";
|
|
4
4
|
import { sanitizeHtml } from "../../composables/sanitizeHtml";
|
|
5
5
|
const $q = useQuasar();
|
|
@@ -63,6 +63,197 @@ const insertTableCMD = () => {
|
|
|
63
63
|
edit.runCmd("insertHTML", tableHTML);
|
|
64
64
|
edit.focus();
|
|
65
65
|
};
|
|
66
|
+
const toAbsoluteUrl = (value) => {
|
|
67
|
+
const url = value?.trim();
|
|
68
|
+
if (!url) return "";
|
|
69
|
+
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//") || url.startsWith("data:") || url.startsWith("blob:")) {
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
return url.startsWith("/") ? `${window.location.origin}${url}` : `${window.location.origin}/${url}`;
|
|
73
|
+
};
|
|
74
|
+
const buildLinkHtml = (payload) => {
|
|
75
|
+
const anchor = document.createElement("a");
|
|
76
|
+
anchor.href = payload.url.trim();
|
|
77
|
+
anchor.textContent = payload.text?.trim() || payload.url.trim();
|
|
78
|
+
if (payload.title?.trim()) anchor.title = payload.title.trim();
|
|
79
|
+
if (payload.target === "_blank") {
|
|
80
|
+
anchor.target = "_blank";
|
|
81
|
+
anchor.rel = "noopener noreferrer";
|
|
82
|
+
}
|
|
83
|
+
return anchor.outerHTML;
|
|
84
|
+
};
|
|
85
|
+
const buildImageHtml = (payload) => {
|
|
86
|
+
const image = document.createElement("img");
|
|
87
|
+
image.setAttribute("src", payload.source.trim());
|
|
88
|
+
if (payload.alt?.trim()) image.setAttribute("alt", payload.alt.trim());
|
|
89
|
+
const width = Number(payload.width);
|
|
90
|
+
if (Number.isFinite(width) && width > 0) {
|
|
91
|
+
image.setAttribute("width", String(Math.round(width)));
|
|
92
|
+
}
|
|
93
|
+
const height = Number(payload.height);
|
|
94
|
+
if (Number.isFinite(height) && height > 0) {
|
|
95
|
+
image.setAttribute("height", String(Math.round(height)));
|
|
96
|
+
}
|
|
97
|
+
return image.outerHTML;
|
|
98
|
+
};
|
|
99
|
+
const selectedImageNode = ref(null);
|
|
100
|
+
const selectedLinkNode = ref(null);
|
|
101
|
+
const updateSelectionState = (e) => {
|
|
102
|
+
let target = e?.target;
|
|
103
|
+
if (target instanceof HTMLImageElement) {
|
|
104
|
+
selectedImageNode.value = target;
|
|
105
|
+
selectedLinkNode.value = target.closest("a");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (target && target.closest) {
|
|
109
|
+
const anchor = target.closest("a");
|
|
110
|
+
if (anchor) {
|
|
111
|
+
selectedLinkNode.value = anchor;
|
|
112
|
+
selectedImageNode.value = null;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const selection = window.getSelection();
|
|
117
|
+
if (!selection || !selection.rangeCount) {
|
|
118
|
+
selectedLinkNode.value = null;
|
|
119
|
+
selectedImageNode.value = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let node = selection.focusNode;
|
|
123
|
+
if (!node) return;
|
|
124
|
+
let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
125
|
+
selectedLinkNode.value = el?.closest("a");
|
|
126
|
+
if (el instanceof HTMLImageElement) {
|
|
127
|
+
selectedImageNode.value = el;
|
|
128
|
+
} else if (el && selection.anchorNode === selection.focusNode) {
|
|
129
|
+
const offset = selection.focusOffset;
|
|
130
|
+
if (el.childNodes.length > offset) {
|
|
131
|
+
const child = el.childNodes[offset];
|
|
132
|
+
if (child instanceof HTMLImageElement) {
|
|
133
|
+
selectedImageNode.value = child;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const anchorOffset = selection.anchorOffset;
|
|
138
|
+
if (el.childNodes.length > anchorOffset) {
|
|
139
|
+
const child = el.childNodes[anchorOffset];
|
|
140
|
+
if (child instanceof HTMLImageElement) {
|
|
141
|
+
selectedImageNode.value = child;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
selectedImageNode.value = null;
|
|
146
|
+
} else {
|
|
147
|
+
selectedImageNode.value = null;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const onEditorClick = (e) => {
|
|
151
|
+
updateSelectionState(e);
|
|
152
|
+
};
|
|
153
|
+
const onEditorDblClick = (e) => {
|
|
154
|
+
updateSelectionState(e);
|
|
155
|
+
if (selectedImageNode.value) {
|
|
156
|
+
insertEditImageCMD();
|
|
157
|
+
} else if (selectedLinkNode.value) {
|
|
158
|
+
insertEditLinkCMD();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const openInsertEditImageDialog = (initialSource = "", skipSaveCaret = false) => {
|
|
162
|
+
const edit = editorRef.value;
|
|
163
|
+
if (!skipSaveCaret) {
|
|
164
|
+
edit.caret.save();
|
|
165
|
+
}
|
|
166
|
+
let initialAlt = "";
|
|
167
|
+
let initialWidth = null;
|
|
168
|
+
let initialHeight = null;
|
|
169
|
+
if (selectedImageNode.value && !initialSource) {
|
|
170
|
+
initialSource = selectedImageNode.value.getAttribute("src") || "";
|
|
171
|
+
initialAlt = selectedImageNode.value.getAttribute("alt") || "";
|
|
172
|
+
initialWidth = selectedImageNode.value.getAttribute("width") || selectedImageNode.value.clientWidth;
|
|
173
|
+
initialHeight = selectedImageNode.value.getAttribute("height") || selectedImageNode.value.clientHeight;
|
|
174
|
+
}
|
|
175
|
+
$q.dialog({
|
|
176
|
+
component: defineAsyncComponent(() => import("./DialogInsertEditImage.vue")),
|
|
177
|
+
componentProps: { initialSource, initialAlt, initialWidth, initialHeight }
|
|
178
|
+
}).onOk((payload) => {
|
|
179
|
+
if (!payload?.source?.trim()) return;
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
edit.focus();
|
|
182
|
+
edit.caret.restore();
|
|
183
|
+
if (selectedImageNode.value && !skipSaveCaret) {
|
|
184
|
+
selectedImageNode.value.setAttribute("src", payload.source.trim());
|
|
185
|
+
if (payload.alt?.trim()) selectedImageNode.value.setAttribute("alt", payload.alt.trim());
|
|
186
|
+
else selectedImageNode.value.removeAttribute("alt");
|
|
187
|
+
if (payload.width) selectedImageNode.value.setAttribute("width", String(Math.round(payload.width)));
|
|
188
|
+
else selectedImageNode.value.removeAttribute("width");
|
|
189
|
+
if (payload.height) selectedImageNode.value.setAttribute("height", String(Math.round(payload.height)));
|
|
190
|
+
else selectedImageNode.value.removeAttribute("height");
|
|
191
|
+
emit("update:modelValue", edit.$el.querySelector(".q-editor__content").innerHTML);
|
|
192
|
+
} else {
|
|
193
|
+
edit.runCmd("insertHTML", buildImageHtml(payload));
|
|
194
|
+
}
|
|
195
|
+
}, 100);
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
const insertEditLinkCMD = () => {
|
|
199
|
+
const edit = editorRef.value;
|
|
200
|
+
edit.caret.save();
|
|
201
|
+
let initialUrl = "";
|
|
202
|
+
let initialText = window.getSelection()?.toString() || "";
|
|
203
|
+
let initialTitle = "";
|
|
204
|
+
let initialTarget = "_self";
|
|
205
|
+
if (selectedLinkNode.value) {
|
|
206
|
+
initialUrl = selectedLinkNode.value.getAttribute("href") || "";
|
|
207
|
+
initialText = selectedLinkNode.value.textContent || initialText;
|
|
208
|
+
initialTitle = selectedLinkNode.value.getAttribute("title") || "";
|
|
209
|
+
initialTarget = selectedLinkNode.value.getAttribute("target") === "_blank" ? "_blank" : "_self";
|
|
210
|
+
}
|
|
211
|
+
$q.dialog({
|
|
212
|
+
component: defineAsyncComponent(() => import("./DialogInsertEditLink.vue")),
|
|
213
|
+
componentProps: { initialUrl, initialText, initialTitle, initialTarget }
|
|
214
|
+
}).onOk((payload) => {
|
|
215
|
+
if (!payload?.url?.trim()) return;
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
edit.focus();
|
|
218
|
+
edit.caret.restore();
|
|
219
|
+
if (selectedLinkNode.value) {
|
|
220
|
+
selectedLinkNode.value.setAttribute("href", payload.url.trim());
|
|
221
|
+
selectedLinkNode.value.textContent = payload.text?.trim() || payload.url.trim();
|
|
222
|
+
if (payload.title?.trim()) selectedLinkNode.value.setAttribute("title", payload.title.trim());
|
|
223
|
+
else selectedLinkNode.value.removeAttribute("title");
|
|
224
|
+
if (payload.target === "_blank") {
|
|
225
|
+
selectedLinkNode.value.setAttribute("target", "_blank");
|
|
226
|
+
selectedLinkNode.value.setAttribute("rel", "noopener noreferrer");
|
|
227
|
+
} else {
|
|
228
|
+
selectedLinkNode.value.removeAttribute("target");
|
|
229
|
+
selectedLinkNode.value.removeAttribute("rel");
|
|
230
|
+
}
|
|
231
|
+
emit("update:modelValue", edit.$el.querySelector(".q-editor__content").innerHTML);
|
|
232
|
+
} else {
|
|
233
|
+
edit.runCmd("insertHTML", buildLinkHtml(payload));
|
|
234
|
+
}
|
|
235
|
+
}, 100);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
const insertEditImageCMD = () => {
|
|
239
|
+
openInsertEditImageDialog();
|
|
240
|
+
};
|
|
241
|
+
const insertImageFromFileManagerCMD = () => {
|
|
242
|
+
const edit = editorRef.value;
|
|
243
|
+
edit.caret.save();
|
|
244
|
+
$q.dialog({
|
|
245
|
+
component: defineAsyncComponent(() => import("./DialogSelectFileFromFileManager.vue"))
|
|
246
|
+
}).onOk((result) => {
|
|
247
|
+
if (!result?.path?.trim()) return;
|
|
248
|
+
let url = "";
|
|
249
|
+
if (result.row && result.row.publicUrl) {
|
|
250
|
+
url = result.row.publicUrl;
|
|
251
|
+
} else {
|
|
252
|
+
url = toAbsoluteUrl(result.path);
|
|
253
|
+
}
|
|
254
|
+
openInsertEditImageDialog(url, true);
|
|
255
|
+
});
|
|
256
|
+
};
|
|
66
257
|
const emit = defineEmits(["update:modelValue"]);
|
|
67
258
|
const props = defineProps({
|
|
68
259
|
fullscreen: { type: Boolean, required: false, skipCheck: true },
|
|
@@ -109,6 +300,10 @@ const attrs = computed(() => {
|
|
|
109
300
|
const a = { ...props };
|
|
110
301
|
if (props.toolbar === void 0) a.toolbar = newToolBar;
|
|
111
302
|
if (props.fonts === void 0) a.fonts = newFonts;
|
|
303
|
+
a.definitions = {
|
|
304
|
+
...newDefinitions,
|
|
305
|
+
...props.definitions
|
|
306
|
+
};
|
|
112
307
|
return a;
|
|
113
308
|
});
|
|
114
309
|
const newToolBar = [
|
|
@@ -128,7 +323,7 @@ const newToolBar = [
|
|
|
128
323
|
}
|
|
129
324
|
],
|
|
130
325
|
["bold", "italic", "strike", "underline", "subscript", "superscript"],
|
|
131
|
-
["token", "hr", "
|
|
326
|
+
["token", "hr", "insertEditLink", "insertEditImage", "insertImageFromFileManager"],
|
|
132
327
|
//['print'],
|
|
133
328
|
["fullscreen"],
|
|
134
329
|
[
|
|
@@ -185,10 +380,49 @@ const newFonts = {
|
|
|
185
380
|
times_new_roman: "Times New Roman",
|
|
186
381
|
verdana: "Verdana"
|
|
187
382
|
};
|
|
383
|
+
const newDefinitions = {
|
|
384
|
+
insertEditLink: {
|
|
385
|
+
tip: "Insert/Edit Link",
|
|
386
|
+
icon: "sym_o_link",
|
|
387
|
+
handler: insertEditLinkCMD
|
|
388
|
+
},
|
|
389
|
+
insertEditImage: {
|
|
390
|
+
tip: "Insert/Edit Image",
|
|
391
|
+
icon: "sym_o_add_photo_alternate",
|
|
392
|
+
handler: insertEditImageCMD
|
|
393
|
+
},
|
|
394
|
+
insertImageFromFileManager: {
|
|
395
|
+
tip: "Insert image from FileManager",
|
|
396
|
+
icon: "sym_o_folder_open",
|
|
397
|
+
handler: insertImageFromFileManagerCMD
|
|
398
|
+
}
|
|
399
|
+
};
|
|
188
400
|
</script>
|
|
189
401
|
|
|
190
402
|
<template>
|
|
191
|
-
<q-editor v-model="localValue" ref="editorRef" v-bind="attrs" @blur="onEditorBlur">
|
|
403
|
+
<q-editor v-model="localValue" ref="editorRef" v-bind="attrs" @blur="onEditorBlur" @click="onEditorClick" @keyup="onEditorClick" @dblclick="onEditorDblClick">
|
|
404
|
+
<template v-slot:insertEditImage>
|
|
405
|
+
<q-btn dense no-caps no-wrap
|
|
406
|
+
:flat="!selectedImageNode"
|
|
407
|
+
:unelevated="!!selectedImageNode"
|
|
408
|
+
:color="selectedImageNode ? 'primary' : 'grey-8'"
|
|
409
|
+
icon="sym_o_add_photo_alternate"
|
|
410
|
+
size="sm"
|
|
411
|
+
@click="insertEditImageCMD">
|
|
412
|
+
<q-tooltip>Insert/Edit Image</q-tooltip>
|
|
413
|
+
</q-btn>
|
|
414
|
+
</template>
|
|
415
|
+
<template v-slot:insertEditLink>
|
|
416
|
+
<q-btn dense no-caps no-wrap
|
|
417
|
+
:flat="!selectedLinkNode"
|
|
418
|
+
:unelevated="!!selectedLinkNode"
|
|
419
|
+
:color="selectedLinkNode ? 'primary' : 'grey-8'"
|
|
420
|
+
icon="sym_o_link"
|
|
421
|
+
size="sm"
|
|
422
|
+
@click="insertEditLinkCMD">
|
|
423
|
+
<q-tooltip>Insert/Edit Link</q-tooltip>
|
|
424
|
+
</q-btn>
|
|
425
|
+
</template>
|
|
192
426
|
<template v-slot:fontSize>
|
|
193
427
|
<q-btn-dropdown dense no-caps ref="fontSizeRef" no-wrap unelevated color="white" text-color="default"
|
|
194
428
|
label="Font Size" icon="sym_o_format_size" size="sm">
|
|
@@ -210,18 +444,6 @@ const newFonts = {
|
|
|
210
444
|
</q-item-section>
|
|
211
445
|
<q-item-section>
|
|
212
446
|
<q-color v-model="foreColor" no-header no-footer style="max-width: 250px;" />
|
|
213
|
-
<!-- :palette="[
|
|
214
|
-
'#ff0000',
|
|
215
|
-
'#ff8000',
|
|
216
|
-
'#ffff00',
|
|
217
|
-
'#00ff00',
|
|
218
|
-
'#00ff80',
|
|
219
|
-
'#00ffff',
|
|
220
|
-
'#0080ff',
|
|
221
|
-
'#0000ff',
|
|
222
|
-
'#8000ff',
|
|
223
|
-
'#ff00ff'
|
|
224
|
-
]" -->
|
|
225
447
|
</q-item-section>
|
|
226
448
|
</q-item>
|
|
227
449
|
</q-btn-dropdown>
|