@hostlink/nuxt-light 1.69.0 → 1.70.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.69.0",
4
+ "version": "1.70.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
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<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
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
- declare var __VLS_38: {}, __VLS_245: {}, __VLS_356: {}, __VLS_363: {};
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 __VLS_245) => any;
31
+ 'user-menu'?: (props: typeof __VLS_340) => any;
6
32
  } & {
7
- 'page-top'?: (props: typeof __VLS_356) => any;
33
+ 'page-top'?: (props: typeof __VLS_482) => any;
8
34
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_363) => any;
35
+ 'page-bottom'?: (props: typeof __VLS_489) => any;
10
36
  };
11
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
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<{}> & Readonly<{
39
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
40
  onLogout?: ((...args: any[]) => any) | undefined;
15
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
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
- declare var __VLS_38: {}, __VLS_245: {}, __VLS_356: {}, __VLS_363: {};
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 __VLS_245) => any;
31
+ 'user-menu'?: (props: typeof __VLS_340) => any;
6
32
  } & {
7
- 'page-top'?: (props: typeof __VLS_356) => any;
33
+ 'page-top'?: (props: typeof __VLS_482) => any;
8
34
  } & {
9
- 'page-bottom'?: (props: typeof __VLS_363) => any;
35
+ 'page-bottom'?: (props: typeof __VLS_489) => any;
10
36
  };
11
- declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
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<{}> & Readonly<{
39
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
40
  onLogout?: ((...args: any[]) => any) | undefined;
15
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
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,208 @@ 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
+ let dialogOpening = false;
154
+ const onEditorDblClick = (e) => {
155
+ e.stopPropagation();
156
+ if (dialogOpening) return;
157
+ updateSelectionState(e);
158
+ if (selectedImageNode.value) {
159
+ dialogOpening = true;
160
+ insertEditImageCMD();
161
+ setTimeout(() => {
162
+ dialogOpening = false;
163
+ }, 300);
164
+ } else if (selectedLinkNode.value) {
165
+ dialogOpening = true;
166
+ insertEditLinkCMD();
167
+ setTimeout(() => {
168
+ dialogOpening = false;
169
+ }, 300);
170
+ }
171
+ };
172
+ const openInsertEditImageDialog = (initialSource = "", skipSaveCaret = false) => {
173
+ const edit = editorRef.value;
174
+ if (!skipSaveCaret) {
175
+ edit.caret.save();
176
+ }
177
+ let initialAlt = "";
178
+ let initialWidth = null;
179
+ let initialHeight = null;
180
+ if (selectedImageNode.value && !initialSource) {
181
+ initialSource = selectedImageNode.value.getAttribute("src") || "";
182
+ initialAlt = selectedImageNode.value.getAttribute("alt") || "";
183
+ initialWidth = selectedImageNode.value.getAttribute("width") || selectedImageNode.value.clientWidth;
184
+ initialHeight = selectedImageNode.value.getAttribute("height") || selectedImageNode.value.clientHeight;
185
+ }
186
+ $q.dialog({
187
+ component: defineAsyncComponent(() => import("./DialogInsertEditImage.vue")),
188
+ componentProps: { initialSource, initialAlt, initialWidth, initialHeight }
189
+ }).onOk((payload) => {
190
+ if (!payload?.source?.trim()) return;
191
+ setTimeout(() => {
192
+ edit.focus();
193
+ edit.caret.restore();
194
+ if (selectedImageNode.value && !skipSaveCaret) {
195
+ selectedImageNode.value.setAttribute("src", payload.source.trim());
196
+ if (payload.alt?.trim()) selectedImageNode.value.setAttribute("alt", payload.alt.trim());
197
+ else selectedImageNode.value.removeAttribute("alt");
198
+ if (payload.width) selectedImageNode.value.setAttribute("width", String(Math.round(payload.width)));
199
+ else selectedImageNode.value.removeAttribute("width");
200
+ if (payload.height) selectedImageNode.value.setAttribute("height", String(Math.round(payload.height)));
201
+ else selectedImageNode.value.removeAttribute("height");
202
+ emit("update:modelValue", edit.$el.querySelector(".q-editor__content").innerHTML);
203
+ } else {
204
+ edit.runCmd("insertHTML", buildImageHtml(payload));
205
+ }
206
+ }, 100);
207
+ });
208
+ };
209
+ const insertEditLinkCMD = () => {
210
+ const edit = editorRef.value;
211
+ edit.caret.save();
212
+ let initialUrl = "";
213
+ let initialText = window.getSelection()?.toString() || "";
214
+ let initialTitle = "";
215
+ let initialTarget = "_self";
216
+ if (selectedLinkNode.value) {
217
+ initialUrl = selectedLinkNode.value.getAttribute("href") || "";
218
+ initialText = selectedLinkNode.value.textContent || initialText;
219
+ initialTitle = selectedLinkNode.value.getAttribute("title") || "";
220
+ initialTarget = selectedLinkNode.value.getAttribute("target") === "_blank" ? "_blank" : "_self";
221
+ }
222
+ $q.dialog({
223
+ component: defineAsyncComponent(() => import("./DialogInsertEditLink.vue")),
224
+ componentProps: { initialUrl, initialText, initialTitle, initialTarget }
225
+ }).onOk((payload) => {
226
+ if (!payload?.url?.trim()) return;
227
+ setTimeout(() => {
228
+ edit.focus();
229
+ edit.caret.restore();
230
+ if (selectedLinkNode.value) {
231
+ selectedLinkNode.value.setAttribute("href", payload.url.trim());
232
+ selectedLinkNode.value.textContent = payload.text?.trim() || payload.url.trim();
233
+ if (payload.title?.trim()) selectedLinkNode.value.setAttribute("title", payload.title.trim());
234
+ else selectedLinkNode.value.removeAttribute("title");
235
+ if (payload.target === "_blank") {
236
+ selectedLinkNode.value.setAttribute("target", "_blank");
237
+ selectedLinkNode.value.setAttribute("rel", "noopener noreferrer");
238
+ } else {
239
+ selectedLinkNode.value.removeAttribute("target");
240
+ selectedLinkNode.value.removeAttribute("rel");
241
+ }
242
+ emit("update:modelValue", edit.$el.querySelector(".q-editor__content").innerHTML);
243
+ } else {
244
+ edit.runCmd("insertHTML", buildLinkHtml(payload));
245
+ }
246
+ }, 100);
247
+ });
248
+ };
249
+ const insertEditImageCMD = () => {
250
+ openInsertEditImageDialog();
251
+ };
252
+ const insertImageFromFileManagerCMD = () => {
253
+ const edit = editorRef.value;
254
+ edit.caret.save();
255
+ $q.dialog({
256
+ component: defineAsyncComponent(() => import("./DialogSelectFileFromFileManager.vue"))
257
+ }).onOk((result) => {
258
+ if (!result?.path?.trim()) return;
259
+ let url = "";
260
+ if (result.row && result.row.publicUrl) {
261
+ url = result.row.publicUrl;
262
+ } else {
263
+ url = toAbsoluteUrl(result.path);
264
+ }
265
+ openInsertEditImageDialog(url, true);
266
+ });
267
+ };
66
268
  const emit = defineEmits(["update:modelValue"]);
67
269
  const props = defineProps({
68
270
  fullscreen: { type: Boolean, required: false, skipCheck: true },
@@ -109,6 +311,10 @@ const attrs = computed(() => {
109
311
  const a = { ...props };
110
312
  if (props.toolbar === void 0) a.toolbar = newToolBar;
111
313
  if (props.fonts === void 0) a.fonts = newFonts;
314
+ a.definitions = {
315
+ ...newDefinitions,
316
+ ...props.definitions
317
+ };
112
318
  return a;
113
319
  });
114
320
  const newToolBar = [
@@ -128,7 +334,7 @@ const newToolBar = [
128
334
  }
129
335
  ],
130
336
  ["bold", "italic", "strike", "underline", "subscript", "superscript"],
131
- ["token", "hr", "link"],
337
+ ["token", "hr", "insertEditLink", "insertEditImage", "insertImageFromFileManager"],
132
338
  //['print'],
133
339
  ["fullscreen"],
134
340
  [
@@ -185,10 +391,49 @@ const newFonts = {
185
391
  times_new_roman: "Times New Roman",
186
392
  verdana: "Verdana"
187
393
  };
394
+ const newDefinitions = {
395
+ insertEditLink: {
396
+ tip: "Insert/Edit Link",
397
+ icon: "sym_o_link",
398
+ handler: insertEditLinkCMD
399
+ },
400
+ insertEditImage: {
401
+ tip: "Insert/Edit Image",
402
+ icon: "sym_o_add_photo_alternate",
403
+ handler: insertEditImageCMD
404
+ },
405
+ insertImageFromFileManager: {
406
+ tip: "Insert image from FileManager",
407
+ icon: "sym_o_folder_open",
408
+ handler: insertImageFromFileManagerCMD
409
+ }
410
+ };
188
411
  </script>
189
412
 
190
413
  <template>
191
- <q-editor v-model="localValue" ref="editorRef" v-bind="attrs" @blur="onEditorBlur">
414
+ <q-editor v-model="localValue" ref="editorRef" v-bind="attrs" @blur="onEditorBlur" @click="onEditorClick" @keyup="onEditorClick" @dblclick="onEditorDblClick">
415
+ <template v-slot:insertEditImage>
416
+ <q-btn dense no-caps no-wrap
417
+ :flat="!selectedImageNode"
418
+ :unelevated="!!selectedImageNode"
419
+ :color="selectedImageNode ? 'primary' : 'grey-8'"
420
+ icon="sym_o_add_photo_alternate"
421
+ size="sm"
422
+ @click="insertEditImageCMD">
423
+ <q-tooltip>Insert/Edit Image</q-tooltip>
424
+ </q-btn>
425
+ </template>
426
+ <template v-slot:insertEditLink>
427
+ <q-btn dense no-caps no-wrap
428
+ :flat="!selectedLinkNode"
429
+ :unelevated="!!selectedLinkNode"
430
+ :color="selectedLinkNode ? 'primary' : 'grey-8'"
431
+ icon="sym_o_link"
432
+ size="sm"
433
+ @click="insertEditLinkCMD">
434
+ <q-tooltip>Insert/Edit Link</q-tooltip>
435
+ </q-btn>
436
+ </template>
192
437
  <template v-slot:fontSize>
193
438
  <q-btn-dropdown dense no-caps ref="fontSizeRef" no-wrap unelevated color="white" text-color="default"
194
439
  label="Font Size" icon="sym_o_format_size" size="sm">
@@ -210,18 +455,6 @@ const newFonts = {
210
455
  </q-item-section>
211
456
  <q-item-section>
212
457
  <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
458
  </q-item-section>
226
459
  </q-item>
227
460
  </q-btn-dropdown>
@@ -167,7 +167,7 @@ const onDblclickRow = (evt, row, index) => {
167
167
  return;
168
168
  }
169
169
  if (row.__typename == "File") {
170
- emit("input", row.path);
170
+ emit("input", row.path, row);
171
171
  }
172
172
  };
173
173
  const findFolder = (path, folders2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostlink/nuxt-light",
3
- "version": "1.69.0",
3
+ "version": "1.70.2",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": {
6
6
  "type": "git",