@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "light",
3
3
  "configKey": "light",
4
- "version": "1.69.0",
4
+ "version": "1.70.1",
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,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", "link"],
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>
@@ -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.1",
4
4
  "description": "HostLink Nuxt Light Framework",
5
5
  "repository": {
6
6
  "type": "git",