@hostlink/nuxt-light 1.68.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/module.mjs +3 -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 +55 -1
- 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/dist/runtime/components/L/System/Setting/mail.vue +15 -8
- package/dist/runtime/components/L/System/Setting/modules.d.vue.ts +1 -0
- package/dist/runtime/components/L/System/Setting/modules.vue +6 -2
- package/dist/runtime/components/L/System/Setting/modules.vue.d.ts +1 -0
- package/dist/runtime/pages/System/view_as.vue +1 -1
- package/dist/runtime/pages/page_not_found.d.vue.ts +3 -0
- package/dist/runtime/pages/page_not_found.vue +26 -0
- package/dist/runtime/pages/page_not_found.vue.d.ts +3 -0
- package/package.json +3 -2
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -236,7 +236,9 @@ export default defineNuxtPlugin(() => {})`;
|
|
|
236
236
|
import { query as q } from '@hostlink/light'
|
|
237
237
|
export default defineNuxtPlugin(() => {
|
|
238
238
|
addRouteMiddleware("auth", async (to) => {
|
|
239
|
-
|
|
239
|
+
const normalize = (p) => (p || "").replace(/[/]+$/, "");
|
|
240
|
+
const publicPaths = ["/", "/page_not_found", "/login"].map(normalize);
|
|
241
|
+
if (publicPaths.includes(normalize(to.path))) return;
|
|
240
242
|
const { my } = await q({
|
|
241
243
|
my: { allowedPath: { __args: { path: to.path } } }
|
|
242
244
|
});
|
|
@@ -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();
|
|
@@ -28,6 +32,7 @@ const tt = await q({
|
|
|
28
32
|
copyrightName: true,
|
|
29
33
|
copyrightYear: true,
|
|
30
34
|
hasFavorite: true,
|
|
35
|
+
notificationBellEnabled: true,
|
|
31
36
|
i18nMessages: {
|
|
32
37
|
name: true,
|
|
33
38
|
value: true
|
|
@@ -254,6 +259,45 @@ const onLogout = async () => {
|
|
|
254
259
|
|
|
255
260
|
<slot name="header"></slot>
|
|
256
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
|
+
|
|
257
301
|
<q-btn :icon="isFav ? 'favorite' : 'sym_o_favorite'" round flat dense class="q-mr-xs"
|
|
258
302
|
@click="onToggleFav" v-if="app.hasFavorite">
|
|
259
303
|
</q-btn>
|
|
@@ -288,7 +332,7 @@ const onLogout = async () => {
|
|
|
288
332
|
</q-menu>
|
|
289
333
|
</q-btn>
|
|
290
334
|
|
|
291
|
-
<l-notification-bell class="q-mr-xs" />
|
|
335
|
+
<l-notification-bell class="q-mr-xs" v-if="app.notificationBellEnabled !== false" />
|
|
292
336
|
|
|
293
337
|
<div class="q-mx-sm" v-if="$q.screen.gt.xs">
|
|
294
338
|
<div class="text-bold text-right">
|
|
@@ -336,6 +380,16 @@ const onLogout = async () => {
|
|
|
336
380
|
|
|
337
381
|
<slot name="user-menu"></slot>
|
|
338
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
|
+
|
|
339
393
|
<q-separator />
|
|
340
394
|
|
|
341
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>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { q } from "#imports";
|
|
2
3
|
const emits = defineEmits(["submit"]);
|
|
3
4
|
defineProps({
|
|
4
5
|
mail_driver: { type: String, required: true, default: "mail" },
|
|
@@ -12,12 +13,6 @@ defineProps({
|
|
|
12
13
|
mail_reply_to: { type: String, required: true },
|
|
13
14
|
mail_reply_to_name: { type: String, required: true }
|
|
14
15
|
});
|
|
15
|
-
const onLoginGmail = () => {
|
|
16
|
-
let state = encodeURIComponent(window.self.location.origin + window.self.location.pathname + "?mail_driver=gmail");
|
|
17
|
-
let scope = encodeURIComponent("https://mail.google.com/");
|
|
18
|
-
let url = "https://accounts.google.com/o/oauth2/v2/auth?scope=" + scope + "&access_type=offline&state=" + state + "&response_type=code&redirect_uri=https%3A%2F%2Fraymond4.hostlink.com.hk%2Flight%2Fgmail_notification%2Fredirect.php&client_id=790028313082-8qqnoqvkqtqssufto11k6qe6pnievcpv.apps.googleusercontent.com&prompt=consent";
|
|
19
|
-
window.open(url, "_blank");
|
|
20
|
-
};
|
|
21
16
|
</script>
|
|
22
17
|
|
|
23
18
|
<template>
|
|
@@ -35,7 +30,19 @@ const onLoginGmail = () => {
|
|
|
35
30
|
]" validation="required"></FormKit>
|
|
36
31
|
|
|
37
32
|
<template v-if="value.mail_driver === 'gmail'">
|
|
38
|
-
<
|
|
33
|
+
<div class="col-12">
|
|
34
|
+
<q-banner class="bg-info text-white" rounded>
|
|
35
|
+
<div class="text-weight-bold q-mb-xs">How to create an App Password</div>
|
|
36
|
+
<ol class="q-pl-md q-my-none">
|
|
37
|
+
<li>Enable 2-Step Verification in your Google Account Security settings.</li>
|
|
38
|
+
<li>Go to the <a href="https://myaccount.google.com/apppasswords" target="_blank" class="text-white" style="text-decoration: underline;">Google App Passwords page</a>.</li>
|
|
39
|
+
<li>Select app and device, or choose Other (Custom name), then click Generate.</li>
|
|
40
|
+
<li>Copy the 16-character password and paste it below.</li>
|
|
41
|
+
</ol>
|
|
42
|
+
</q-banner>
|
|
43
|
+
</div>
|
|
44
|
+
<FormKit type="l-input" label="Gmail Address" name="mail_username" validation="required|email"></FormKit>
|
|
45
|
+
<FormKit type="l-input" label="App Password" name="mail_password" validation="required" :attrs="{ type: 'password' }"></FormKit>
|
|
39
46
|
</template>
|
|
40
47
|
|
|
41
48
|
<template v-else>
|
|
@@ -50,7 +57,7 @@ const onLoginGmail = () => {
|
|
|
50
57
|
<FormKit type="l-input" label="SMTP Host" name="mail_host"></FormKit>
|
|
51
58
|
<FormKit type="l-input" label="SMTP Port" name="mail_port"></FormKit>
|
|
52
59
|
<FormKit type="l-input" label="SMTP Username" name="mail_username"></FormKit>
|
|
53
|
-
<FormKit type="l-input" label="SMTP Password" name="mail_password"></FormKit>
|
|
60
|
+
<FormKit type="l-input" label="SMTP Password" name="mail_password" :attrs="{ type: 'password' }"></FormKit>
|
|
54
61
|
|
|
55
62
|
<FormKit type="l-select" label="SMTP Encryption" name="mail_encryption" :options="[
|
|
56
63
|
{ label: 'None', value: '' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
file_manager: string;
|
|
3
|
+
notification_bell_enabled: string;
|
|
3
4
|
};
|
|
4
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
6
|
submit: (...args: any[]) => void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
const emits = defineEmits(["submit"]);
|
|
3
3
|
defineProps({
|
|
4
|
-
file_manager: { type: String, required: true }
|
|
4
|
+
file_manager: { type: String, required: true },
|
|
5
|
+
notification_bell_enabled: { type: String, required: true }
|
|
5
6
|
});
|
|
6
7
|
</script>
|
|
7
8
|
|
|
@@ -10,6 +11,9 @@ defineProps({
|
|
|
10
11
|
<l-field :label="$t('File Manager')" stack-label hide-bottom-space class="col-6">
|
|
11
12
|
<form-kit type="l-checkbox" label="Show" name="file_manager" true-value="1" false-value="0" />
|
|
12
13
|
</l-field>
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
<l-field :label="$t('Notification Bell')" stack-label hide-bottom-space class="col-6">
|
|
16
|
+
<form-kit type="l-checkbox" label="Show" name="notification_bell_enabled" true-value="1" false-value="0" />
|
|
17
|
+
</l-field>
|
|
14
18
|
</form-kit>
|
|
15
19
|
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
file_manager: string;
|
|
3
|
+
notification_bell_enabled: string;
|
|
3
4
|
};
|
|
4
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
6
|
submit: (...args: any[]) => void;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const _default: typeof __VLS_export;
|
|
2
|
+
export default _default;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useRoute, navigateTo } from "#imports";
|
|
3
|
+
const route = useRoute();
|
|
4
|
+
const attemptedPath = route.query.path || route.fullPath;
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<l-page class="q-pa-md">
|
|
9
|
+
<q-banner class="bg-negative text-white">
|
|
10
|
+
<template v-slot:avatar>
|
|
11
|
+
<q-icon name="sym_o_error" />
|
|
12
|
+
</template>
|
|
13
|
+
<div class="text-h6">Page not found</div>
|
|
14
|
+
<div v-if="attemptedPath && attemptedPath !== '/'">
|
|
15
|
+
The requested page was not found or you do not have permission to access it:
|
|
16
|
+
<code>{{ attemptedPath }}</code>
|
|
17
|
+
</div>
|
|
18
|
+
<div v-else>
|
|
19
|
+
The requested page was not found or you do not have permission to access it.
|
|
20
|
+
</div>
|
|
21
|
+
</q-banner>
|
|
22
|
+
<div class="q-mt-md">
|
|
23
|
+
<q-btn color="primary" icon="sym_o_home" label="Home" to="/" />
|
|
24
|
+
</div>
|
|
25
|
+
</l-page>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const _default: typeof __VLS_export;
|
|
2
|
+
export default _default;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hostlink/nuxt-light",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.70.1",
|
|
4
4
|
"description": "HostLink Nuxt Light Framework",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@hostlink/light": "^3.2.7",
|
|
42
42
|
"@nuxt/module-builder": "^1.0.1",
|
|
43
43
|
"@quasar/extras": "^1.17.0",
|
|
44
|
-
"@quasar/quasar-ui-qmarkdown": "^
|
|
44
|
+
"@quasar/quasar-ui-qmarkdown": "^3.0.0-rc.1",
|
|
45
45
|
"@vueuse/core": "^14.0.0",
|
|
46
46
|
"axios": "^1.12.2",
|
|
47
47
|
"defu": "^6.1.4",
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"eslint": "^9.39.1",
|
|
66
66
|
"nuxt": "^4.3.0",
|
|
67
67
|
"typescript": "^5.9.2",
|
|
68
|
+
"vitest": "^3.2.6",
|
|
68
69
|
"vue-tsc": "^3.1.5"
|
|
69
70
|
}
|
|
70
71
|
}
|