pull/256/head
Candid Dauth 2023-11-07 02:19:20 +01:00
rodzic b8583ce570
commit 31ca895acc
66 zmienionych plików z 620 dodań i 413 usunięć

Wyświetl plik

@ -28,7 +28,9 @@ Note that client always replaces whole objects rather than updating individual p
```javascript
class ReactiveClient extends Client {
_makeReactive = Vue.reactive;
_makeReactive(object) {
return Vue.reactive(object);
}
}
```
@ -36,8 +38,13 @@ class ReactiveClient extends Client {
```javascript
class ReactiveClient extends Client {
_set = Vue.set;
_delete = Vue.delete;
_set(object, key, value) {
Vue.set(object, key, value);
}
_delete(object, key) {
Vue.delete(object, key);
}
}
```

Wyświetl plik

@ -37,6 +37,7 @@
"test-watch": "vitest"
},
"dependencies": {
"@ckpack/vue-color": "^1.5.0",
"@tmcw/togeojson": "^5.8.1",
"@vitejs/plugin-vue": "^4.4.0",
"blob": "^0.1.0",
@ -73,7 +74,6 @@
"vite-plugin-css-injected-by-js": "^3.3.0",
"vite-plugin-dts": "^3.6.0",
"vue": "^3.3.4",
"vue-color": "^2.8.1",
"vuedraggable": "next"
},
"devDependencies": {

Wyświetl plik

@ -10,7 +10,9 @@
import { injectContextRequired } from "./facil-map-context-provider/facil-map-context-provider.vue";
class ReactiveClient extends Client {
_makeReactive = reactive as any;
_makeReactive<O extends object>(obj: O) {
return reactive(obj) as O;
}
};
</script>

Wyświetl plik

@ -44,14 +44,16 @@
>
<p>Here you can set an advanced expression to show/hide certain markers/lines based on their attributes. The filter expression only applies to your view of the map, but it can be persisted as part of a saved view or a shared link.</p>
<textarea
class="form-control text-monospace"
v-model="filter"
rows="5"
v-validity="validationError"
></textarea>
<div class="invalid-feedback" v-if="validationError">
<pre>{{validationError}}</pre>
<div class="was-validated">
<textarea
class="form-control text-monospace"
v-model="filter"
rows="5"
v-validity="validationError"
></textarea>
<div class="invalid-feedback" v-if="validationError">
<pre>{{validationError}}</pre>
</div>
</div>
<hr />

Wyświetl plik

@ -1,8 +1,7 @@
<script setup lang="ts">
import type { ID } from "facilmap-types";
import { canControl, getUniqueId, mergeObject, validateRequired } from "../utils/utils";
import { clone } from "facilmap-utils";
import { isEqual, omit } from "lodash-es";
import { cloneDeep, isEqual, omit } from "lodash-es";
import ModalDialog from "./ui/modal-dialog.vue";
import ColourField from "./ui/colour-field.vue";
import FieldInput from "./ui/field-input.vue";
@ -31,7 +30,7 @@
const originalLine = toRef(() => client.value.lines[props.lineId]);
const line = ref(clone(originalLine.value));
const line = ref(cloneDeep(originalLine.value));
const isModified = computed(() => !isEqual(line.value, originalLine.value));
@ -69,6 +68,7 @@
:isModified="isModified"
@submit="$event.waitUntil(save())"
@hidden="emit('hidden')"
ref="modalRef"
>
<template #default>
<div class="row mb-3">
@ -102,7 +102,11 @@
<div class="row mb-3">
<label :for="`${id}-width-input`" class="col-sm-3 col-form-label">Width</label>
<div class="col-sm-9">
<WidthField :id="`${id}-width-input`" v-model="line.width"></WidthField>
<WidthField
:id="`${id}-width-input`"
v-model="line.width"
class="fm-form-range-with-label"
></WidthField>
</div>
</div>
</template>

Wyświetl plik

@ -1,8 +1,7 @@
<script setup lang="ts">
import type { ID } from "facilmap-types";
import { canControl, getUniqueId, mergeObject, validateRequired } from "../utils/utils";
import { clone } from "facilmap-utils";
import { isEqual } from "lodash-es";
import { cloneDeep, isEqual } from "lodash-es";
import ModalDialog from "./ui/modal-dialog.vue";
import ColourField from "./ui/colour-field.vue";
import SymbolField from "./ui/symbol-field.vue";
@ -31,7 +30,7 @@
const originalMarker = toRef(() => client.value.markers[props.markerId]);
const marker = ref(clone(originalMarker.value));
const marker = ref(cloneDeep(originalMarker.value));
const isModified = computed(() => !isEqual(marker.value, client.value.markers[props.markerId]));
@ -67,6 +66,7 @@
title="Edit Marker"
class="fm-edit-marker"
:isModified="isModified"
ref="modalRef"
@submit="$event.waitUntil(save())"
@hidden="emit('hidden')"
>
@ -82,7 +82,11 @@
<div class="row mb-3">
<label :for="`${id}-colour-input`" class="col-sm-3 col-form-label">Colour</label>
<div class="col-sm-9">
<ColourField :id="`${id}-colour-input`" v-model="marker.colour" :validationError="colourValidationError"></ColourField>
<ColourField
:id="`${id}-colour-input`"
v-model="marker.colour"
:validationError="colourValidationError"
></ColourField>
</div>
</div>
</template>
@ -91,7 +95,11 @@
<div class="row mb-3">
<label :for="`${id}-size-input`" class="col-sm-3 col-form-label">Size</label>
<div class="col-sm-9">
<SizeField :id="`${id}-size-input`" v-model="marker.size"></SizeField>
<SizeField
:id="`${id}-size-input`"
v-model="marker.size"
class="fm-form-range-with-label"
></SizeField>
</div>
</div>
</template>

Wyświetl plik

@ -1,9 +1,8 @@
<script setup lang="ts">
import type { Field, ID, Type } from "facilmap-types";
import { clone } from "facilmap-utils";
import { canControl, getUniqueId, validateRequired, validations } from "../../utils/utils";
import { mergeTypeObject } from "./edit-type-utils";
import { isEqual } from "lodash-es";
import { cloneDeep, isEqual } from "lodash-es";
import { useToasts } from "../ui/toasts/toasts.vue";
import ColourField from "../ui/colour-field.vue";
import ShapeField from "../ui/shape-field.vue";
@ -17,7 +16,7 @@
import EditTypeDropdownDialog from "./edit-type-dropdown-dialog.vue";
import { computed, ref, watch } from "vue";
import ModalDialog from "../ui/modal-dialog.vue";
import vValidity from "../ui/validated-form/validity";
import vValidity, { vValidityContext } from "../ui/validated-form/validity";
import { showConfirm } from "../ui/alert.vue";
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
@ -43,7 +42,7 @@
});
const initialType = computed<Type>(() => {
const type = isCreate.value ? { fields: [] } as any : clone(originalType.value)!;
const type = isCreate.value ? { fields: [] } as any : cloneDeep(originalType.value)!;
for(const field of type.fields) {
field.oldName = field.name;
@ -52,7 +51,7 @@
return type;
});
const type = ref(clone(initialType.value));
const type = ref(cloneDeep(initialType.value));
const editField = ref<Field>();
const modalRef = ref<InstanceType<typeof ModalDialog>>();
@ -184,9 +183,9 @@
>
<div class="row mb-3">
<label :for="`${id}-name-input`" class="col-sm-3 col-form-label">Name</label>
<div class="col-sm-9">
<div class="col-sm-9" v-validity-context>
<input class="form-control" :id="`${id}-name-input`" v-model="type.name" v-validity="nameValidationError" />
<div class="invalid-feedback" v-if="nameValidationError">
<div class="invalid-feedback">
{{nameValidationError}}
</div>
</div>
@ -194,7 +193,7 @@
<div class="row mb-3">
<label :for="`${id}-type-input`" class="col-sm-3 col-form-label">Type</label>
<div class="col-sm-9">
<div class="col-sm-9" v-validity-context>
<select
:id="`${id}-type-input`"
v-model="type.type"
@ -205,7 +204,7 @@
<option value="marker">Marker</option>
<option value="line">Line</option>
</select>
<div class="invalid-feedback" v-if="typeValidationError">
<div class="invalid-feedback">
{{typeValidationError}}
</div>
</div>
@ -233,13 +232,15 @@
></ColourField>
</div>
<div class="col-sm-3">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-colour-fixed`"
v-model="type.colourFixed"
/>
<label :for="`${id}-default-colour-fixed`" class="form-check-label">Fixed</label>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-colour-fixed`"
v-model="type.colourFixed"
/>
<label :for="`${id}-default-colour-fixed`" class="form-check-label">Fixed</label>
</div>
</div>
</div>
</div>
@ -259,13 +260,15 @@
></SizeField>
</div>
<div class="col-sm-3">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-size-fixed`"
v-model="type.sizeFixed"
/>
<label :for="`${id}-default-size-fixed`" class="form-check-label">Fixed</label>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-size-fixed`"
v-model="type.sizeFixed"
/>
<label :for="`${id}-default-size-fixed`" class="form-check-label">Fixed</label>
</div>
</div>
</div>
</div>
@ -285,13 +288,15 @@
></SymbolField>
</div>
<div class="col-sm-3">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-symbol-fixed`"
v-model="type.symbolFixed"
/>
<label :for="`${id}-default-symbol-fixed`" class="form-check-label">Fixed</label>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-symbol-fixed`"
v-model="type.symbolFixed"
/>
<label :for="`${id}-default-symbol-fixed`" class="form-check-label">Fixed</label>
</div>
</div>
</div>
</div>
@ -311,13 +316,15 @@
></ShapeField>
</div>
<div class="col-sm-3">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-shape-fixed`"
v-model="type.shapeFixed"
/>
<label :for="`${id}-default-shape-fixed`" class="form-check-label">Fixed</label>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-shape-fixed`"
v-model="type.shapeFixed"
/>
<label :for="`${id}-default-shape-fixed`" class="form-check-label">Fixed</label>
</div>
</div>
</div>
</div>
@ -337,13 +344,15 @@
></WidthField>
</div>
<div class="col-sm-3">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-width-fixed`"
v-model="type.widthFixed"
/>
<label :for="`${id}-default-width-fixed`" class="form-check-label">Fixed</label>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-width-fixed`"
v-model="type.widthFixed"
/>
<label :for="`${id}-default-width-fixed`" class="form-check-label">Fixed</label>
</div>
</div>
</div>
</div>
@ -363,13 +372,15 @@
></RouteMode>
</div>
<div class="col-sm-3">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-mode-fixed`"
v-model="type.modeFixed"
/>
<label :for="`${id}-default-mode-fixed`" class="form-check-label">Fixed</label>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-default-mode-fixed`"
v-model="type.modeFixed"
/>
<label :for="`${id}-default-mode-fixed`" class="form-check-label">Fixed</label>
</div>
</div>
</div>
</div>
@ -382,13 +393,15 @@
<div class="row mb-3">
<label :for="`${id}-show-in-legend-input`" class="col-sm-3 col-form-label">Legend</label>
<div class="col-sm-9">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-show-in-legend-input`"
v-model="type.showInLegend"
/>
<label :for="`${id}-show-in-legend-input`" class="form-check-label">Show in legend</label>
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-show-in-legend-input`"
v-model="type.showInLegend"
/>
<label :for="`${id}-show-in-legend-input`" class="form-check-label">Show in legend</label>
</div>
<div class="form-text">
An item for this type will be shown in the legend. Any fixed style attributes are applied to it. Dropdown or checkbox fields that control the style generate additional legend items.
</div>
@ -415,13 +428,13 @@
>
<template #item="{ element: field, index: idx }">
<tr>
<td>
<td v-validity-context>
<input
class="form-control"
v-model="field.name"
v-validity="fieldValidationErrors[idx].name"
/>
<div class="invalid-feedback" v-if="fieldValidationErrors[idx].name">
<div class="invalid-feedback">
{{fieldValidationErrors[idx].name}}
</div>
</td>

Wyświetl plik

@ -1,8 +1,7 @@
<script setup lang="ts">
import type { Field, FieldOption, FieldOptionUpdate, FieldUpdate, Type } from "facilmap-types";
import { clone } from "facilmap-utils";
import { canControl, getUniqueId, mergeObject, validateRequired } from "../../utils/utils";
import { isEqual } from "lodash-es";
import { cloneDeep, isEqual } from "lodash-es";
import ColourField from "../ui/colour-field.vue";
import Draggable from "vuedraggable";
import Icon from "../ui/icon.vue";
@ -15,6 +14,7 @@
import { computed, ref, watch } from "vue";
import { showConfirm } from "../ui/alert.vue";
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { vValidityContext } from "../ui/validated-form/validity";
function getControlNumber(type: Type, field: FieldUpdate): number {
return [
@ -48,7 +48,7 @@
const modalRef = ref<InstanceType<typeof ModalDialog>>();
const initialField = computed(() => {
const field: FieldUpdate = clone(props.field);
const field: FieldUpdate = cloneDeep(props.field);
if(field.type == 'checkbox') {
if(!field.options || field.options.length != 2) {
@ -71,7 +71,7 @@
return field;
});
const fieldValue = ref(clone(initialField.value));
const fieldValue = ref(cloneDeep(initialField.value));
watch(() => props.field, (newField, oldField) => {
if (fieldValue.value) {
@ -149,7 +149,7 @@
<div class="row mb-3">
<label class="col-sm-3 col-form-label">Control</label>
<div class="col-sm-9">
<div class="form-check">
<div class="form-check fm-form-check-with-label">
<input
:id="`${id}-control-colour`"
class="form-check-input"
@ -255,9 +255,9 @@
<td v-if="fieldValue.type == 'checkbox'">
<strong>{{idx === 0 ? '✘' : '✔'}}</strong>
</td>
<td class="field">
<td class="field" v-validity-context>
<input class="form-control" v-model="option.value" v-validity="optionValidationErrors![idx].value" />
<div class="invalid-feedback" v-if="optionValidationErrors![idx].value">
<div class="invalid-feedback">
{{optionValidationErrors![idx].value}}
</div>
</td>
@ -292,7 +292,7 @@
</tfoot>
</table>
<div class="invalid-feedback" v-if="validationError">
<div class="fm-form-invalid-feedback" v-if="validationError">
{{validationError}}
</div>
</ModalDialog>

Wyświetl plik

@ -1,6 +1,6 @@
import type { CRU, FieldUpdate, Type } from "facilmap-types";
import { clone } from "facilmap-utils";
import { mergeObject } from "../../utils/utils";
import { cloneDeep } from "lodash-es";
function getIdxForInsertingField(targetFields: FieldUpdate[], targetField: FieldUpdate, mergedFields: FieldUpdate[]): number {
// Check which field comes after the field in the target field list, and return the index of that field in mergedFields
@ -27,7 +27,7 @@ function mergeFields(oldFields: FieldUpdate[], newFields: FieldUpdate[], customF
else if(!customField)
return Object.assign({}, newField, {oldName: newField.name});
let mergedField = clone(customField);
let mergedField = cloneDeep(customField);
mergeObject(oldField, newField, mergedField);
return mergedField;
@ -41,7 +41,7 @@ function mergeFields(oldFields: FieldUpdate[], newFields: FieldUpdate[], customF
}
export function mergeTypeObject(oldObject: Type, newObject: Type, targetObject: Type & Type<CRU.UPDATE>): void {
let customFields = clone(targetObject.fields);
let customFields = cloneDeep(targetObject.fields);
mergeObject(oldObject, newObject, targetObject);

Wyświetl plik

@ -4,6 +4,7 @@
import SearchBoxTab from "../search-box/search-box-tab.vue";
import { useEventListener } from "../../utils/utils";
import { injectContextRequired, requireClientContext, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { normalizeLineName } from "facilmap-utils";
const context = injectContextRequired();
const client = requireClientContext(context);
@ -31,7 +32,7 @@
const title = computed(() => {
if (line.value != null)
return line.value.name;
return normalizeLineName(line.value.name);
else
return undefined;
});

Wyświetl plik

@ -145,7 +145,7 @@
<a v-if="showBackButton" href="javascript:" @click="emit('back')"><Icon icon="arrow-left"></Icon></a>
{{normalizeLineName(line.name)}}
</h2>
<div v-if="!isMoving" class="btn-group">
<div v-if="!isMoving" class="btn-toolbar">
<button
v-if="line.ascent != null"
type="button"
@ -180,7 +180,7 @@
<ElevationPlot :route="line" v-if="line.ascent != null && showElevationPlot"></ElevationPlot>
</div>
<div v-if="!isMoving" class="btn-group">
<div v-if="!isMoving" class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="line"

Wyświetl plik

@ -64,7 +64,7 @@
<td>{{type.name}}</td>
<td>{{type.type}}</td>
<td class="td-buttons">
<div class="btn-group">
<div class="btn-toolbar">
<button
type="button"
class="btn btn-secondary"

Wyświetl plik

@ -4,6 +4,7 @@
import SearchBoxTab from "../search-box/search-box-tab.vue"
import { useEventListener } from "../../utils/utils";
import { injectContextRequired, requireClientContext, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { normalizeMarkerName } from "facilmap-utils";
const context = injectContextRequired();
const client = requireClientContext(context);
@ -40,7 +41,7 @@
<template v-if="markerId">
<SearchBoxTab
:id="`fm${context.id}-marker-info-tab`"
:title="marker?.name ?? ''"
:title="marker ? normalizeMarkerName(marker.name) : ''"
isCloseable
@close="close()"
>

Wyświetl plik

@ -21,7 +21,7 @@
const props = withDefaults(defineProps<{
markerId: ID;
showBackButton: boolean;
showBackButton?: boolean;
}>(), {
showBackButton: false
});
@ -89,7 +89,7 @@
</template>
</dl>
<div class="btn-group">
<div class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="marker"

Wyświetl plik

@ -111,7 +111,7 @@
</li>
</ul>
<div class="btn-group">
<div class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="selection"

Wyświetl plik

@ -9,7 +9,7 @@
import { getUniqueId } from "../utils/utils";
import ValidatedForm from "./ui/validated-form/validated-form.vue";
import pDebounce from "p-debounce";
import vValidity from "./ui/validated-form/validity";
import vValidity, { vValidityContext } from "./ui/validated-form/validity";
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import type { FacilMapContext } from "./facil-map-context-provider/facil-map-context";
@ -132,7 +132,7 @@
@hidden="emit('hidden')"
>
<p>Enter the link or ID of an existing collaborative map here to open that map.</p>
<div class="input-group">
<div class="input-group has-validation" v-validity-context>
<input
class="form-control"
v-model="padId"
@ -148,9 +148,9 @@
<div v-if="openFormRef?.formData.isValidating" class="spinner-border spinner-border-sm"></div>
Open
</button>
</div>
<div class="invalid-feedback" v-if="openFormError">
{{openFormError}}
<div class="invalid-feedback">
{{openFormError}}
</div>
</div>
<hr/>

Wyświetl plik

@ -103,7 +103,7 @@
</li>
</ul>
<div v-if="client.padData && !client.readonly" class="btn-group">
<div v-if="client.padData && !client.readonly" class="btn-toolbar">
<ZoomToObjectButton
v-if="zoomDestination"
label="selection"

Wyświetl plik

@ -5,6 +5,7 @@
import copyToClipboard from "copy-to-clipboard";
import { useToasts } from "../ui/toasts/toasts.vue";
import { injectContextRequired } from "../facil-map-context-provider/facil-map-context-provider.vue";
import vValidity, { vValidityContext } from "../ui/validated-form/validity";
const idProps = ["id", "writeId", "adminId"] as const;
type IdProp = typeof idProps[number];
@ -55,10 +56,10 @@
</script>
<template>
<div class="row mb-3" :class="{ 'was-validated': touched }">
<div class="row mb-3" v-validity-context>
<label :for="`${id}-input`" class="col-sm-3 col-form-label">{{props.label}}</label>
<div class="col-sm-9">
<div class="input-group">
<div class="input-group has-validation">
<input
:id="`${id}-input`"
class="form-control fm-pad-settings-pad-id-edit"
@ -67,15 +68,15 @@
v-validity="error"
@input="touched = true"
@blur="touched = true"
>
/>
<button
class="btn btn-secondary"
type="button"
@click="copy(context.baseUrl + encodeURIComponent(padData[idProp]))"
>Copy</button>
</div>
<div v-if="error" class="invalid-feedback">
{{error}}
<div class="invalid-feedback">
{{error}}
</div>
</div>
<div v-if="!error" class="form-text">
{{props.description}}

Wyświetl plik

@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import type { CRU, PadData } from "facilmap-types";
import { clone, generateRandomPadId } from "facilmap-utils";
import { generateRandomPadId } from "facilmap-utils";
import { getUniqueId, mergeObject } from "../../utils/utils";
import { isEqual } from "lodash-es";
import { cloneDeep, isEqual } from "lodash-es";
import ModalDialog from "../ui/modal-dialog.vue";
import { useToasts } from "../ui/toasts/toasts.vue";
import { showConfirm } from "../ui/alert.vue";
@ -28,7 +28,8 @@
const id = getUniqueId("fm-pad-settings");
const isDeleting = ref(false);
const deleteConfirmation = ref("");
const padData = ref<PadData<CRU.CREATE>>(props.isCreate ? {
const initialPadData: PadData<CRU.CREATE> | undefined = props.isCreate ? {
name: "New FacilMap",
searchEngines: false,
description: "",
@ -39,11 +40,15 @@
legend1: "",
legend2: "",
defaultViewId: null
} : clone(client.value.padData) as PadData<CRU.CREATE>);
} : undefined;
const originalPadData = computed(() => props.isCreate ? initialPadData! : client.value.padData as PadData<CRU.CREATE>);
const padData = ref(cloneDeep(originalPadData.value));
const modalRef = ref<InstanceType<typeof ModalDialog>>();
const isModified = computed(() => !isEqual(padData.value, client.value.padData));
const isModified = computed(() => !isEqual(padData.value, originalPadData.value));
watch(() => client.value.padData, (newPadData, oldPadData) => {
if (!props.isCreate && padData.value && newPadData)
@ -58,7 +63,6 @@
await client.value.createPad(padData.value as PadData<CRU.CREATE>);
else
await client.value.editPad(padData.value);
modalRef.value?.modal.hide();
} catch (err) {
toasts.showErrorToast(`fm${context.id}-pad-settings-error`, props.isCreate ? "Error creating map" : "Error saving map settings", err);
@ -91,12 +95,13 @@
<template>
<ModalDialog
:title="isCreate ? 'Create collaborative map' : 'Map settings'"
:title="props.isCreate ? 'Create collaborative map' : 'Map settings'"
class="fm-pad-settings"
:noCancel="noCancel"
:noCancel="props.noCancel"
:isBusy="isDeleting"
:isCreate="isCreate"
:isCreate="props.isCreate"
:isModified="isModified"
:okLabel="props.isCreate ? 'Create' : undefined"
ref="modalRef"
@submit="$event.waitUntil(save())"
@hidden="emit('hidden')"
@ -129,15 +134,29 @@
<div class="row mb-3">
<label :for="`${id}-pad-name-input`" class="col-sm-3 col-form-label">Map name</label>
<div class="col-sm-9">
<input :id="`${id}-pad-name-input`" class="form-control" type="text" v-model="padData.name">
<input
:id="`${id}-pad-name-input`"
class="form-control"
type="text"
v-model="padData.name"
/>
</div>
</div>
<div class="row mb-3">
<label :for="`${id}-search-engines-input`" class="col-sm-3 col-form-label">Search engines</label>
<div class="col-sm-9">
<input :id="`${id}-search-engines-input`" class="form-check-input" type="checkbox" v-model="padData.searchEngines">
<label :for="`${id}-search-engines-input`" class="form-check-label">Accessible for search engines</label>
<div class="form-check fm-form-check-with-label">
<input
:id="`${id}-search-engines-input`"
class="form-check-input"
type="checkbox"
v-model="padData.searchEngines"
/>
<label :for="`${id}-search-engines-input`" class="form-check-label">
Accessible for search engines
</label>
</div>
<div class="form-text">
If this is enabled, search engines like Google will be allowed to add the read-only version of this map.
</div>
@ -147,7 +166,12 @@
<div class="row mb-3">
<label :for="`${id}-description-input`" class="col-sm-3 col-form-label">Short description</label>
<div class="col-sm-9">
<input :id="`${id}-description-input`" class="form-control" type="text" v-model="padData.description">
<input
:id="`${id}-description-input`"
class="form-control"
type="text"
v-model="padData.description"
/>
<div class="form-text">
This description will be shown under the result in search engines.
</div>
@ -157,8 +181,17 @@
<div class="row mb-3">
<label :for="`${id}-cluster-markers-input`" class="col-sm-3 col-form-label">Search engines</label>
<div class="col-sm-9">
<input :id="`${id}-cluster-markers-input`" class="form-check-input" type="checkbox" v-model="padData.clusterMarkers">
<label :for="`${id}-cluster-markers-input`" class="form-check-label">Cluster markers</label>
<div class="form-check fm-form-check-with-label">
<input
:id="`${id}-cluster-markers-input`"
class="form-check-input"
type="checkbox"
v-model="padData.clusterMarkers"
/>
<label :for="`${id}-cluster-markers-input`" class="form-check-label">
Cluster markers
</label>
</div>
<div class="form-text">
If enabled, when there are many markers in one area, they will be replaced by a placeholder at low zoom levels. This improves performance on maps with many markers.
</div>
@ -168,8 +201,18 @@
<div class="row mb-3">
<label :for="`${id}-legend1-input`" class="col-sm-3 col-form-label">Legend text</label>
<div class="col-sm-9">
<textarea :id="`${id}-legend1-input`" class="form-control" type="text" v-model="padData.legend1"></textarea>
<textarea :id="`${id}-legend2-input`" class="form-control" type="text" v-model="padData.legend2"></textarea>
<textarea
:id="`${id}-legend1-input`"
class="form-control"
type="text"
v-model="padData.legend1"
></textarea>
<textarea
:id="`${id}-legend2-input`"
class="form-control mt-1"
type="text"
v-model="padData.legend2"
></textarea>
<div class="form-text">
Text that will be shown above and below the legend. Can be formatted with <a href="http://commonmark.org/help/" target="_blank">Markdown</a>.
</div>
@ -177,15 +220,26 @@
</div>
</template>
<template v-if="padData && !isCreate">
<template v-if="padData && !props.isCreate">
<hr/>
<div class="row mb-3">
<label :for="`${id}-delete-input`" class="col-sm-3 col-form-label">Delete map</label>
<div class="col-sm-9">
<div class="input-group">
<input :form="`${id}-delete-form`" :id="`${id}-delete-input`" class="form-control" type="text" v-model="deleteConfirmation">
<button :form="`${id}-delete-form`" class="btn btn-danger" type="submit" :disabled="isDeleting || modalRef?.formData?.isSubmitting || deleteConfirmation != 'DELETE'">
<input
:form="`${id}-delete-form`"
:id="`${id}-delete-input`"
class="form-control"
type="text"
v-model="deleteConfirmation"
/>
<button
:form="`${id}-delete-form`"
class="btn btn-danger"
type="submit"
:disabled="isDeleting || modalRef?.formData?.isSubmitting || deleteConfirmation != 'DELETE'"
>
<div v-if="isDeleting" class="spinner-border spinner-border-sm"></div>
Delete map
</button>

Wyświetl plik

@ -3,7 +3,7 @@
import ModalDialog from "./ui/modal-dialog.vue";
import { useToasts } from "./ui/toasts/toasts.vue";
import { computed, ref } from "vue";
import vValidity from "./ui/validated-form/validity";
import vValidity, { vValidityContext } from "./ui/validated-form/validity";
import { getUniqueId } from "../utils/utils";
import { round } from "facilmap-utils";
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
@ -78,7 +78,7 @@
>
<div class="row mb-3">
<label :for="`${id}-name-input`" class="col-sm-3 col-form-label">Name</label>
<div class="col-sm-9">
<div class="col-sm-9" v-validity-context>
<input
class="form-control"
:id="`${id}-name-input`"
@ -86,7 +86,7 @@
v-validity="nameError"
autofocus
/>
<div class="invalid-feedback" v-if="nameError">
<div class="invalid-feedback">
{{nameError}}
</div>
</div>
@ -158,15 +158,17 @@
<div class="row mb-3">
<label :for="`${id}-overpass-input`" class="col-sm-3 col-form-label">POIs</label>
<div class="col-sm-9">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-overpass-input`"
v-model="includeOverpass"
/>
<label class="form-check-label" :for="`${id}-overpass-input`">
Include POIs (<code v-if="mapContext.overpassIsCustom">{{mapContext.overpassCustom}}</code><template v-else>{{mapContext.overpassPresets.map((p) => p.label).join(', ')}}</template>)
</label>
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-overpass-input`"
v-model="includeOverpass"
/>
<label class="form-check-label" :for="`${id}-overpass-input`">
Include POIs (<code v-if="mapContext.overpassIsCustom">{{mapContext.overpassCustom}}</code><template v-else>{{mapContext.overpassPresets.map((p) => p.label).join(', ')}}</template>)
</label>
</div>
</div>
</div>
</template>
@ -184,15 +186,17 @@
<div class="row mb-3">
<label :for="`${id}-filter-checkbox`" class="col-sm-3 col-form-label">Filter</label>
<div class="col-sm-9">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-filter-checkbox`"
v-model="includeFilter"
/>
<label :for="`${id}-filter-checkbox`" class="form-check-label">
Include current filter (<code>{{mapContext.filter}}</code>)
</label>
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-filter-checkbox`"
v-model="includeFilter"
/>
<label :for="`${id}-filter-checkbox`" class="form-check-label">
Include current filter (<code>{{mapContext.filter}}</code>)
</label>
</div>
</div>
</div>
</template>
@ -200,13 +204,15 @@
<div class="row mb-3">
<label :for="`${id}-make-default-input`" class="col-sm-3 col-form-label">Default view</label>
<div class="col-sm-9">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-make-default-input`"
v-model="makeDefault"
/>
<label :for="`${id}-make-default-input`" class="form-check-label">Make default view</label>
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"
:id="`${id}-make-default-input`"
v-model="makeDefault"
/>
<label :for="`${id}-make-default-input`" class="form-check-label">Make default view</label>
</div>
</div>
</div>
</ModalDialog>

Wyświetl plik

@ -73,6 +73,7 @@
const cardHeaderRef = ref<HTMLElement>();
const resizeHandleRef = ref<HTMLElement>();
const isPanning = ref<boolean>();
const panStartHeight = ref<number>();
const restoreHeight = ref<number>();
const resizeStartHeight = ref<number>();
@ -99,6 +100,7 @@
});
function handlePanStart(): void {
isPanning.value = true;
restoreHeight.value = undefined;
panStartHeight.value = parseInt($(containerRef.value!).css("flex-basis"));
}
@ -109,6 +111,7 @@
}
function handlePanEnd(): void {
isPanning.value = false;
mapContext.value.components.map.invalidateSize({ pan: false });
}
@ -186,7 +189,7 @@
class="card fm-search-box"
v-show="searchBoxContext.tabs.size > 0"
ref="containerRef"
:class="{ isNarrow: context.isNarrow, hasFocus }"
:class="{ isNarrow: context.isNarrow, hasFocus, isPanning }"
@focusin="handleFocusIn"
@focusout="handleFocusOut"
@transitionend="handleTransitionEnd"
@ -270,9 +273,12 @@
&.isNarrow {
min-height: 55px;
flex-basis: 55px;
transition: flex-basis 0.4s;
overflow: hidden;
&:not(.isPanning) {
transition: flex-basis 0.4s;
}
height: auto !important; /* Override resize height from non-narrow mode */
width: auto !important; /* Override resize width from non-narrow mode */

Wyświetl plik

@ -231,7 +231,7 @@
<slot name="after"></slot>
<div v-if="client.padData && !client.readonly && searchResults && searchResults.length > 0" class="btn-group">
<div v-if="client.padData && !client.readonly && searchResults && searchResults.length > 0" class="btn-toolbar">
<button
type="button"
class="btn btn-secondary"

Wyświetl plik

@ -90,7 +90,7 @@
<div class="row mb-3">
<label class="col-sm-3 col-form-label">Settings</label>
<div class="col-sm-9">
<div class="form-check">
<div class="form-check fm-form-check-with-label">
<input
type="checkbox"
class="form-check-input"

Wyświetl plik

@ -72,25 +72,27 @@
getContent: () => h('div', {
class: touched.value ? 'was-validated' : ''
}, [
withDirectives(h('input', {
type: "text",
class: `form-control${touched.value ? ' was-validated' : ''}`,
value: value.value,
onInput: (e: InputEvent) => {
value.value = (e.target as HTMLInputElement).value;
touched.value = true;
},
onBlur: () => {
touched.value = true;
},
autofocus: true,
ref: inputRef
}), [
[vValidity, validationError.value]
]),
...(validationError.value ? [h('div', {
withDirectives(
h('input', {
type: "text",
class: `form-control${touched.value ? ' was-validated' : ''}`,
value: value.value,
onInput: (e: InputEvent) => {
value.value = (e.target as HTMLInputElement).value;
touched.value = true;
},
onBlur: () => {
touched.value = true;
},
autofocus: true,
ref: inputRef
}), [
[vValidity, validationError.value]
]
),
h('div', {
class: "invalid-feedback"
}, validationError.value)] : [])
}, validationError.value)
]),
onShown: () => {
inputRef.value!.focus();

Wyświetl plik

@ -1,12 +1,13 @@
<script lang="ts">
import { ColorMixin, Hue, Saturation } from "vue-color";
import ColorMixin from "@ckpack/vue-color/src/mixin/color.js";
import { Hue, Saturation } from "@ckpack/vue-color";
import Picker from "./picker.vue";
import { makeTextColour } from "facilmap-utils";
import { arrowNavigation } from "../../utils/ui";
import { StyleValue, computed, nextTick, ref } from "vue";
function normalizeData(value: string) {
return ColorMixin.data.apply({ value }).val;
return ColorMixin.data.apply({ modelValue: value }).val;
}
function isValidColour(colour?: string) {
@ -15,7 +16,7 @@
function validateColour(colour: string): string | undefined {
if (!isValidColour(colour)) {
return "Needs to be in 3-digit or 6-digit hex format, for example <code>f00</code> or <code>0000ff</code>.";
return "Needs to be in 3-digit or 6-digit hex format, for example f00 or 0000ff.";
}
}
@ -59,12 +60,12 @@
if (props.validationError) {
return props.validationError;
} else {
return validateColour(val.value);
return validateColour(value.value ?? "");
}
});
function handleChange(val: any): void {
emit('update:modelValue', normalizeData(val).hex.replace(/^#/, '').toLowerCase());
value.value = normalizeData(val).hex.replace(/^#/, '').toLowerCase();
}
function handleKeyDown(event: KeyboardEvent): void {
@ -83,11 +84,13 @@
<template>
<Picker
customClass="fm-colour-field"
v-model="value"
@keydown="handleKeyDown"
:validationError="validationError"
:previewStyle="previewStyle"
>
<template #preview>
<span style="width: 1.4em" :style="previewStyle"></span>
<span style="width: 1.4em"></span>
</template>
<template #default="{ isModal }">

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import { SlotsType, computed, defineComponent, h, ref, useSlots, watch, watchEffect } from "vue";
import { maxSizeModifiers, type ButtonSize, type ButtonVariant, useMaxBreakpoint } from "../../utils/bootstrap";
import { SlotsType, computed, defineComponent, h, ref, shallowRef, useSlots, watch, watchEffect } from "vue";
import { maxSizeModifiers, type ButtonSize, type ButtonVariant, useMaxBreakpoint, PopperConfigFunction } from "../../utils/bootstrap";
import { Dropdown } from "bootstrap";
import vLinkDisabled from "../../utils/link-disabled";
import type { TooltipPlacement } from "../../utils/tooltip";
@ -39,7 +39,7 @@
}>();
const buttonRef = ref<InstanceType<typeof AttributePreservingElement>>();
const dropdownRef = ref<Dropdown>();
const dropdownRef = shallowRef<Dropdown>();
const isNarrow = useMaxBreakpoint("sm");
@ -60,26 +60,22 @@
}
}
type PopperConfig = ReturnType<Dropdown.PopperConfigFunction>;
watch(() => buttonRef.value?.elementRef, (newRef, oldRef, onCleanup) => {
if (newRef) {
const dropdown = new CustomDropdown(newRef, {
popperConfig: ((defaultConfig: PopperConfig): PopperConfig => {
const result: PopperConfig = {
...defaultConfig,
modifiers: [
...(defaultConfig.modifiers ?? []),
...maxSizeModifiers
],
strategy: "fixed"
};
return result;
}) as any // Typing of popperConfig is wrong and does not contain the function argument
const popperConfig: PopperConfigFunction = (defaultConfig) => ({
...defaultConfig,
modifiers: [
...(defaultConfig.modifiers ?? []),
...maxSizeModifiers
],
strategy: "fixed"
});
dropdownRef.value = new CustomDropdown(newRef, {
popperConfig: popperConfig as any
});
dropdownRef.value = dropdown;
onCleanup(() => {
dropdown.dispose();
dropdownRef.value!.dispose();
dropdownRef.value = undefined;
});
}
}, { immediate: true });

Wyświetl plik

@ -73,17 +73,17 @@
<template>
<div class="bb-popover">
<span ref="trigger" @click="handleClick()">
<div ref="trigger" @click="handleClick()">
<slot name="trigger"></slot>
</span>
</div>
<Popover
:show="showPopover"
@update:show="handleShowPopoverChange"
:element="trigger"
:class="props.customClass"
hide-on-outside-click
:enforce-element-width="props.enforceElementWidth"
hideOnOutsideClick
:enforceElementWidth="props.enforceElementWidth"
>
<template v-slot:header>
{{props.title}}

Wyświetl plik

@ -26,12 +26,15 @@
submit: [event: CustomSubmitEvent];
}>();
const modalRef = ref<HTMLElement>();
const modal = useModal(modalRef, { emit });
const validatedFormRef = ref<InstanceType<typeof ValidatedForm>>();
const isSubmitting = computed(() => validatedFormRef.value?.formData.isSubmitting);
const modalRef = ref<HTMLElement>();
const modal = useModal(modalRef, {
emit,
static: computed(() => isSubmitting.value || props.isBusy || props.noCancel || props.isModified)
});
function handleSubmit(event: CustomSubmitEvent) {
emit("submit", event);
}
@ -54,21 +57,21 @@
tabindex="-1"
aria-hidden="true"
ref="modalRef"
:data-bs-backdrop="isSubmitting || isBusy || props.noCancel ? 'static' : 'true'"
:data-bs-keyboard="isSubmitting || isBusy || noCancel || isModified ? 'false' : 'true'"
v-on="{
'hide.bs.modal': (e: any) => {
if (isSubmitting || isBusy) {
e.preventDefault();
}
}
}"
:data-bs-backdrop="isSubmitting || props.isBusy || props.noCancel || props.isModified ? 'static' : 'true'"
:data-bs-keyboard="isSubmitting || props.isBusy || props.noCancel || props.isModified ? 'false' : 'true'"
>
<div class="modal-dialog modal-dialog-scrollable">
<ValidatedForm class="modal-content" @submit="handleSubmit" ref="validatedFormRef">
<div class="modal-header">
<h1 class="modal-title fs-5">{{props.title}}</h1>
<button v-if="!noCancel" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button
v-if="!props.noCancel"
:disabled="isSubmitting || props.isBusy"
@click="modal.hide()"
type="button"
class="btn-close"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<slot v-bind="expose"></slot>
@ -79,19 +82,19 @@
<div style="flex-grow: 1"></div>
<button
v-if="!noCancel"
v-if="!props.noCancel"
type="button"
class="btn btn-secondary"
:class="isModified || isCreate ? 'btn-secondary' : 'btn-primary'"
:class="props.isModified || props.isCreate ? 'btn-secondary' : 'btn-primary'"
@click="modal.hide()"
:disabled="isSubmitting || isBusy"
>{{isModified || isCreate ? 'Cancel' : 'Close'}}</button>
:disabled="isSubmitting || props.isBusy"
>{{props.isModified || props.isCreate ? 'Cancel' : 'Close'}}</button>
<button
v-if="noCancel || isModified || isCreate"
v-if="props.noCancel || props.isModified || props.isCreate"
type="submit"
class="btn btn-primary"
:disabled="isSubmitting || isBusy"
:disabled="isSubmitting || props.isBusy"
>{{props.okLabel ?? 'Save'}}</button>
</div>
</ValidatedForm>

Wyświetl plik

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from "vue";
import { StyleValue, computed, ref, watchEffect } from "vue";
import HybridPopover from "./hybrid-popover.vue";
import vValidity, { vValidityContext } from "./validated-form/validity";
const props = withDefaults(defineProps<{
id?: string;
@ -10,6 +11,7 @@
modelValue?: string;
/** If true, the width of the popover will be fixed to the width of the element. */
enforceElementWidth?: boolean;
previewStyle?: StyleValue;
}>(), {
enforceElementWidth: false,
disabled: false
@ -80,8 +82,11 @@
:customClass="props.customClass"
>
<template #trigger>
<div class="input-group">
<span class="input-group-text" @click="inputRef?.focus()">
<div class="input-group has-validation" v-validity-context>
<span
class="input-group-text"
@click="inputRef?.focus()"
:style="props.previewStyle">
<slot name="preview"></slot>
</span>
<input
@ -94,10 +99,10 @@
:id="id"
ref="inputRef"
@keydown="handleInputKeyDown"
>
</div>
<div class="invalid-feedback" v-if="props.validationError">
{{props.validationError}}
/>
<div class="invalid-feedback">
{{props.validationError}}
</div>
</div>
</template>

Wyświetl plik

@ -2,6 +2,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { Popover, Tooltip } from "bootstrap";
import { useResizeObserver } from "../../utils/vue";
import type { PopperConfigFunction } from "../../utils/bootstrap";
/**
* Like Bootstrap Popover, but uses an existing popover element rather than creating a new one. This way, the popover
@ -57,10 +58,15 @@
renderPopover.value = true;
await nextTick();
if (props.element) {
const popperConfig: PopperConfigFunction = (defaultConfig) => ({
...defaultConfig,
strategy: "fixed"
});
CustomPopover.getOrCreateInstance(props.element, {
placement: props.placement,
content: popoverContent.value!,
trigger: 'manual'
trigger: 'manual',
popperConfig: popperConfig as any
}).show();
}
};

Wyświetl plik

@ -258,7 +258,7 @@
</div>
</div>
<div class="invalid-feedback" v-if="props.validationError">
<div class="invalid-feedback fm-form-invalid-feedback" v-if="props.validationError">
{{props.validationError}}
</div>
</div>

Wyświetl plik

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from "vue";
import vValidity from "./validated-form/validity";
import vValidity, { vValidityContext } from "./validated-form/validity";
import vTooltip from "../../utils/tooltip";
const props = defineProps<{
modelValue: number | undefined;
@ -28,11 +29,23 @@
</script>
<template>
<input type="range" class="custom-range" min="15" v-model="value" v-validity="validationError" />
<div class="invalid-feedback" v-if="validationError">
{{validationError}}
<div v-validity-context class="fm-size-field">
<input
type="range"
class="custom-range"
min="15"
v-model="value"
v-validity="validationError"
v-tooltip="value != null ? `${value}` : undefined"
/>
<div class="invalid-feedback">
{{validationError}}
</div>
</div>
</template>
<style lang="scss">
.fm-size-field input {
width: 100%;
}
</style>

Wyświetl plik

@ -48,7 +48,7 @@
const toasts = ref<ToastInstance[]>([]);
const toastRefs = reactive(new Map<ToastInstance, HTMLElement>());
export function useToasts(): ToastContext {
export function useToasts(noScope = false): ToastContext {
const contextId = getUniqueId("fm-toast-context");
const result: ToastContext = {
showErrorToast: async (id, title, err, options) => {
@ -106,9 +106,11 @@
}
};
onScopeDispose(() => {
result.dispose();
});
if (!noScope) {
onScopeDispose(() => {
result.dispose();
});
}
return result;
}
@ -142,7 +144,7 @@
</script>
<template>
<div class="toast-container position-fixed top-0 end-0 p-3">
<div class="toast-container position-fixed top-0 end-0 p-3 fm-toasts">
<div
v-for="toast in toasts"
:key="toast.key"
@ -187,9 +189,13 @@
</template>
<style lang="scss">
.fm-toast-actions {
button + button {
margin-left: 5px;
.fm-toasts {
z-index: 10002;
.fm-toast-actions {
button + button {
margin-left: 5px;
}
}
}
</style>

Wyświetl plik

@ -7,6 +7,9 @@ declare global {
interface Element {
_fmValidityPromise?: Promise<string | undefined>;
_fmValidityToasts?: ToastContext;
_fmValidityInputListener?: () => void;
_fmValidityTouched?: boolean;
}
}
@ -15,7 +18,7 @@ type Value = string | undefined | Promise<string | undefined>;
const updateValidity: Directive<FormElement, Value> = (el, binding) => {
if (!el._fmValidityToasts) {
el._fmValidityToasts = useToasts();
el._fmValidityToasts = useToasts(true);
}
const formData = el.form && getValidatedForm(el.form);
@ -57,3 +60,29 @@ const vValidity: Directive<FormElement, Value> = {
};
export default vValidity;
const updateValidityContext: Directive<FormElement, void> = (el, binding) => {
if (!el._fmValidityInputListener) {
el._fmValidityInputListener = () => {
el._fmValidityTouched = true;
el.classList.add("was-validated");
};
el.addEventListener("input", el._fmValidityInputListener);
}
if (el._fmValidityTouched) {
el.classList.add("was-validated");
}
};
export const vValidityContext: Directive<FormElement, void> = {
mounted: updateValidityContext,
updated: updateValidityContext,
beforeUnmount: (el) => {
if (el._fmValidityInputListener) {
el.removeEventListener("input", el._fmValidityInputListener);
delete el._fmValidityInputListener;
}
}
};

Wyświetl plik

@ -1,5 +1,7 @@
<script setup lang="ts">
import { computed } from "vue";
import vValidity, { vValidityContext } from "./validated-form/validity";
import vTooltip from "../../utils/tooltip";
const props = defineProps<{
modelValue: number | undefined;
@ -19,9 +21,18 @@
</script>
<template>
<input type="range" class="custom-range" min="1" v-model="value" v-validity="props.validationError" />
<div class="invalid-feedback" v-if="props.validationError">
{{props.validationError}}
<div v-validity-context>
<input
type="range"
class="custom-range"
min="1"
v-model="value"
v-validity="props.validationError"
v-tooltip="value != null ? `${value}` : undefined"
/>
<div class="invalid-feedback">
{{props.validationError}}
</div>
</div>
</template>

Wyświetl plik

@ -1,3 +1,6 @@
import "./bootstrap.scss";
import "./styles.scss";
import { registerDeobfuscationHandlers } from "../utils/obfuscate";
registerDeobfuscationHandlers();

Wyświetl plik

@ -0,0 +1,30 @@
/**
* Renders a form feedback error that is shown when the form has been validated, regardless of whether
* it is a sibling of a form element.
*/
.fm-form-invalid-feedback {
display: none;
color: var(--bs-form-invalid-color);
}
.was-validated .fm-form-invalid-feedback {
display: block;
}
/**
* Should be applied to form-check elements that have a horizontal form label, in order to be correctly
* aligned with that label.
*/
.fm-form-check-with-label {
// Same padding-top as .col-form-label
padding-top: calc(0.375rem + var(--bs-border-width));
}
/**
* Should be applied to form-check elements that have a horizontal form label, in order to be correctly
* aligned with that label.
*/
.fm-form-range-with-label {
// Same padding-top as .col-form-label plus half line-height (1.5) minus half range input height (16px)
padding-top: calc(0.375rem + var(--bs-border-width) + 0.75rem - 8px);
}

Wyświetl plik

@ -1,6 +1,6 @@
import { computed, Ref, ref } from "vue";
import maxSize from "popper-max-size-modifier";
import type { Modifier, ModifierArguments } from "@popperjs/core";
import type { Modifier, ModifierArguments, Options } from "@popperjs/core";
const breakpointMinWidth = {
// See https://getbootstrap.com/docs/5.3/layout/breakpoints/#available-breakpoints
@ -76,4 +76,10 @@ export const maxSizeModifiers: Array<Partial<Modifier<any, any>>> = [
}
}
}
];
];
/**
* The type of the `popperConfig` configuration option of various Bootstrap components, since the typing is wrong.
* See https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/67333
*/
export type PopperConfigFunction = (defaultConfig: Partial<Options>) => Partial<Options>;

Wyświetl plik

@ -1,5 +1,5 @@
import { Modal } from "bootstrap";
import { onScopeDispose, ref, Ref, watch } from "vue";
import { Ref, shallowRef, watch, watchEffect } from "vue";
export interface ModalConfig {
emit?: {
@ -15,6 +15,8 @@ export interface ModalConfig {
onHide?: (event: Modal.Event) => void;
/** Will be called after the fade-out animation when the modal is closed. */
onHidden?: (event: Modal.Event) => void;
/** If true, the modal will not be closed by clicking the backdrop or pressing Escape. */
static?: Ref<boolean>;
}
export interface ModalActions {
@ -24,8 +26,8 @@ export interface ModalActions {
/**
* Enables a Bootstrap modal dialog on the element that is saved in the returned {@link ModalActions#ref}.
*/
export function useModal(modalRef: Ref<HTMLElement | undefined>, { emit, onShown, onHide }: ModalConfig): ModalActions {
const modal = ref<Modal>();
export function useModal(modalRef: Ref<HTMLElement | undefined>, { emit, onShown, onHide, static: isStatic }: ModalConfig): ModalActions {
const modal = shallowRef<Modal>();
const handleShow = (e: Event) => {
const zIndex = 1 + Math.max(1056, ...[...document.querySelectorAll(".modal")].map((el) => el !== modalRef.value && Number(getComputedStyle(el).zIndex) || -Infinity));
@ -49,30 +51,35 @@ export function useModal(modalRef: Ref<HTMLElement | undefined>, { emit, onShown
}
};
watch(modalRef, (newRef, oldRef) => {
if (modal.value) {
modal.value.dispose();
modal.value = undefined;
}
if (oldRef) {
oldRef.removeEventListener('show.bs.modal', handleShow);
oldRef.removeEventListener('shown.bs.modal', handleShown);
oldRef.removeEventListener('hide.bs.modal', handleHide);
oldRef.removeEventListener('hidden.bs.modal', handleHidden);
}
watch(modalRef, (newRef, oldRef, onCleanup) => {
if (newRef) {
modal.value = new Modal(newRef);
newRef.addEventListener('show.bs.modal', handleShow);
newRef.addEventListener('shown.bs.modal', handleShown);
newRef.addEventListener('hide.bs.modal', handleHide);
newRef.addEventListener('hidden.bs.modal', handleHidden);
onCleanup(() => {
modal.value!.dispose();
modal.value = undefined;
newRef.removeEventListener('show.bs.modal', handleShow);
newRef.removeEventListener('shown.bs.modal', handleShown);
newRef.removeEventListener('hide.bs.modal', handleHide);
newRef.removeEventListener('hidden.bs.modal', handleHidden);
});
show();
}
}, { immediate: true });
});
watchEffect(() => {
if (modal.value) {
const config = (modal.value as any)._config as Modal.Options;
config.backdrop = isStatic?.value ? "static" : true;
config.keyboard = !isStatic?.value;
}
});
const show = () => {
if (!modal.value) {
@ -88,10 +95,6 @@ export function useModal(modalRef: Ref<HTMLElement | undefined>, { emit, onShown
modal.value.hide();
};
onScopeDispose(() => {
modal.value?.dispose();
});
return {
hide
};

Wyświetl plik

@ -1,5 +1,4 @@
import { isEqual } from "lodash-es";
import { clone } from "facilmap-utils";
import { cloneDeep, isEqual } from "lodash-es";
import type { Field, Line, Marker, Type } from "facilmap-types";
import type { Emitter } from "mitt";
import { DeepReadonly, Ref, onBeforeUnmount, onMounted, watchEffect } from "vue";
@ -23,7 +22,7 @@ export function mergeObject<T extends Record<keyof any, any>>(oldObject: T | und
)
mergeObject(oldObject && oldObject[i], newObject[i], targetObject[i]);
else if(oldObject == null || !isEqual(oldObject[i], newObject[i]))
targetObject[i] = clone(newObject[i]);
targetObject[i] = cloneDeep(newObject[i]);
}
}

Wyświetl plik

@ -1,3 +0,0 @@
#b-toaster-top-right {
z-index: 10002;
}

Wyświetl plik

@ -1,9 +1,6 @@
import $ from "jquery";
import { createApp, defineComponent, h, ref, watch } from "vue";
//import { FacilMap } from "../lib";
import FacilMap from "../lib/components/facil-map.vue";
import "./bootstrap.scss";
import "./map.scss";
import { FacilMap } from "../lib";
import { decodeQueryString, encodeQueryString } from "facilmap-utils";
import decodeURIComponent from "decode-uri-component";

Wyświetl plik

@ -1,9 +1,10 @@
declare module "vue-color" {
export const ColorMixin: any;
declare module "@ckpack/vue-color" {
export const Hue: any;
export const Saturation: any;
}
declare module "@ckpack/vue-color/src/mixin/color.js";
declare module "@tmcw/togeojson" {
export const gpx: any;
export const kml: any;

Wyświetl plik

@ -206,7 +206,7 @@ export default class LinesLayer extends FeatureGroup {
if(!this.linesById[line.id]) {
this.linesById[line.id] = new HighlightablePolyline([ ]);
if(line.id != null) { // We don't want a popup for lines that we are drawing right now
if(line.id != null) {
this.linesById[line.id]
.bindTooltip("", { ...tooltipOptions, sticky: true, offset: [ 20, 0 ] })
.on("tooltipopen", () => {
@ -232,6 +232,17 @@ export default class LinesLayer extends FeatureGroup {
(this.linesById[line.id] as any).line = line;
this.linesById[line.id].setLatLngs(splitLatLngs).setStyle(style);
if (line.name && line.id != null) { // We don't want a popup for lines that we are drawing right now
const quoted = quoteHtml(line.name);
if (this.linesById[line.id]._tooltip) {
this.linesById[line.id].setTooltipContent(quoted);
} else {
this.linesById[line.id].bindTooltip(quoted, { ...tooltipOptions, sticky: true, offset: [ 20, 0 ] });
}
} else if (this.linesById[line.id]._tooltip) {
this.linesById[line.id].unbindTooltip();
}
if (!this.hasLayer(this.linesById[line.id]))
this.addLayer(this.linesById[line.id]);
}

Wyświetl plik

@ -133,11 +133,6 @@ export default class MarkersLayer extends MarkerCluster {
const layer = new MarkerLayer([ 0, 0 ]);
this.markersById[marker.id] = layer;
this.addLayer(layer);
layer.bindTooltip("", { ...tooltipOptions, offset: [ 20, -15 ] });
layer.on("tooltipopen", () => {
this.markersById[marker.id].setTooltipContent(quoteHtml(this.client.markers[marker.id].name));
});
}
(this.markersById[marker.id] as any).marker = marker;
@ -148,6 +143,17 @@ export default class MarkersLayer extends MarkerCluster {
this.markersById[marker.id].setLatLng([ marker.lat, marker.lon ]);
this.markersById[marker.id].setStyle({ marker, highlight, raised: highlight });
if (marker.name) {
const quoted = quoteHtml(marker.name);
if (this.markersById[marker.id]._tooltip) {
this.markersById[marker.id].setTooltipContent(quoted);
} else {
this.markersById[marker.id].bindTooltip(quoted, { ...tooltipOptions, offset: [ 20, -15 ] });
}
} else if (this.markersById[marker.id]._tooltip) {
this.markersById[marker.id].unbindTooltip();
}
}
_deleteMarker(marker: ObjectWithId): void {

Wyświetl plik

@ -9,6 +9,7 @@ declare module "leaflet" {
interface GridLayerOptions extends LayerOptions {}
interface Layer {
_tooltip?: Tooltip;
options: LayerOptions;
addInteractiveTarget(targetEl: HTMLElement): void;
removeInteractiveTarget(targetEl: HTMLElement): void;

Wyświetl plik

@ -1,7 +1,7 @@
import { AssociationOptions, Model, ModelAttributeColumnOptions, ModelCtor, WhereOptions, DataTypes, FindOptions, Op, Sequelize, ModelStatic, InferAttributes, InferCreationAttributes, CreationAttributes } from "sequelize";
import { Line, Marker, PadId, ID, Type, Bbox, CRU } from "facilmap-types";
import Database from "./database.js";
import { clone, isEqual } from "lodash-es";
import { cloneDeep, isEqual } from "lodash-es";
import { calculateRouteForLine } from "../routing/routing.js";
import { PadModel } from "./pad";
import { arrayToAsyncIterator } from "../utils/streams";
@ -28,7 +28,7 @@ export function getVirtualLatType(): ModelAttributeColumnOptions {
return this.getDataValue("pos")?.coordinates[1];
},
set(val: number) {
const point = clone(this.getDataValue("pos")) ?? { type: "Point", coordinates: [0, 0] };
const point = cloneDeep(this.getDataValue("pos")) ?? { type: "Point", coordinates: [0, 0] };
point.coordinates[1] = val;
this.setDataValue("pos", point);
}
@ -42,7 +42,7 @@ export function getVirtualLonType(): ModelAttributeColumnOptions {
return this.getDataValue("pos")?.coordinates[0];
},
set(val: number) {
const point = clone(this.getDataValue("pos")) ?? { type: "Point", coordinates: [0, 0] };
const point = cloneDeep(this.getDataValue("pos")) ?? { type: "Point", coordinates: [0, 0] };
point.coordinates[0] = val;
this.setDataValue("pos", point);
}
@ -393,7 +393,7 @@ export default class DatabaseHelpers {
const objectStream = (isLine ? this._db.lines.getPadLinesByType(padId, typeId) : this._db.markers.getPadMarkersByType(padId, typeId));
for await (const object of objectStream) {
const newData = clone(object.data);
const newData = cloneDeep(object.data);
const newNames: string[] = [ ];
for(const oldName in rename) {

Wyświetl plik

@ -2,7 +2,7 @@ import { Model, DataTypes, FindOptions, InferAttributes, CreationOptional, Forei
import Database from "./database.js";
import { HistoryEntry, HistoryEntryAction, HistoryEntryCreate, HistoryEntryType, ID, PadData, PadId } from "facilmap-types";
import { createModel, getDefaultIdType, makeNotNullForeignKey } from "./helpers.js";
import { clone } from "lodash-es";
import { cloneDeep } from "lodash-es";
interface HistoryModel extends Model<InferAttributes<HistoryModel>, InferCreationAttributes<HistoryModel>> {
id: CreationOptional<ID>;
@ -77,7 +77,7 @@ export default class DatabaseHistory {
attributes: [ "id" ]
})).map(it => it.id);
const dataClone = clone(data);
const dataClone = cloneDeep(data);
if(data.type != "Pad") {
if(dataClone.objectBefore) {
delete (dataClone.objectBefore as any).id;

Wyświetl plik

@ -87,7 +87,7 @@ export default class DatabaseLines {
mode : { type: DataTypes.TEXT, allowNull: false, defaultValue: "" },
colour : { type: DataTypes.STRING(6), allowNull: false, defaultValue: "0000ff", validate: validateColour },
width : { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, defaultValue: 4, validate: { min: 1 } },
name : { type: DataTypes.TEXT, allowNull: true },
name : { type: DataTypes.TEXT, allowNull: true, get: function(this: LineModel) { return this.getDataValue("name") || ""; } },
distance : { type: DataTypes.FLOAT(24, 2).UNSIGNED, allowNull: true },
time : { type: DataTypes.INTEGER.UNSIGNED, allowNull: true },
ascent : { type: DataTypes.INTEGER.UNSIGNED, allowNull: true },

Wyświetl plik

@ -38,7 +38,7 @@ export default class DatabaseMarkers {
lat: getVirtualLatType(),
lon: getVirtualLonType(),
pos: getPosType(),
name : { type: DataTypes.TEXT, allowNull: true },
name : { type: DataTypes.TEXT, allowNull: true, get: function(this: MarkerModel) { return this.getDataValue("name") || ""; } },
colour : { type: DataTypes.STRING(6), allowNull: false, defaultValue: "ff0000", validate: validateColour },
size : { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, defaultValue: 25, validate: { min: 15 } },
symbol : { type: DataTypes.TEXT, allowNull: true },

Wyświetl plik

@ -1,6 +1,6 @@
import { generateRandomId, promiseProps } from "../utils/utils.js";
import { CreationAttributes, DataTypes, Op, Utils, col, fn } from "sequelize";
import { clone, isEqual } from "lodash-es";
import { cloneDeep, isEqual } from "lodash-es";
import Database from "./database.js";
import { PadModel } from "./pad.js";
import { LineModel, LinePointModel } from "./line.js";
@ -116,7 +116,7 @@ export default class DatabaseMigrations {
const objectStream = (type.type == "line" ? this._db.lines.getPadLinesByType(type.padId, type.id) : this._db.markers.getPadMarkersByType(type.padId, type.id));
for await (const object of objectStream) {
const newData = clone(object.data);
const newData = cloneDeep(object.data);
for(const dropdown of dropdowns) {
const newVal = (dropdown.options || []).filter((option: any) => option.key == newData[dropdown.name])[0];
if(newVal)

Wyświetl plik

@ -2,7 +2,7 @@ import { jsonStream, asyncIteratorToArray } from "../utils/streams.js";
import { compileExpression, normalizeLineName, normalizeMarkerName } from "facilmap-utils";
import { Marker, MarkerFeature, LineFeature, PadId } from "facilmap-types";
import Database from "../database/database.js";
import { clone, keyBy, mapValues, omit } from "lodash-es";
import { cloneDeep, keyBy, mapValues, omit } from "lodash-es";
import { LineWithTrackPoints } from "../database/line.js";
export async function* exportGeoJson(database: Database, padId: PadId, filter?: string): AsyncGenerator<string, void, void> {
@ -72,7 +72,7 @@ function markerToGeoJson(marker: Marker): MarkerFeature {
size: marker.size,
symbol: marker.symbol,
shape: marker.shape,
data: clone(marker.data),
data: cloneDeep(marker.data),
typeId: marker.typeId
}
};

Wyświetl plik

@ -26,10 +26,10 @@ if(config.maxmindUserId && config.maxmindLicenseKey) {
schedule("0 3 * * *", download);
load().catch((err) => {
console.log("Error loading maxmind database", err.stack || err);
console.log("Error loading maxmind database", err);
});
download().catch((err) => {
console.log("Error downloading maxmind database", err.stack || err);
console.log("Error downloading maxmind database", err);
});
}

Wyświetl plik

@ -47,7 +47,7 @@ export default class Socket {
d.add(socket);
d.on("error", function(err) {
console.error("Uncaught error in socket:", err.stack);
console.error("Uncaught error in socket:", err);
socket.disconnect();
});
@ -150,7 +150,7 @@ class SocketConnection {
unvalidatedSocketHandlers: UnvalidatedSocketHandlers = {
error: (err) => {
console.error("Error! Disconnecting client.");
console.error(err.stack);
console.error(err);
this.socket.disconnect();
},
@ -165,13 +165,13 @@ class SocketConnection {
if(this.route) {
this.database.routes.deleteRoute(this.route.id).catch((err) => {
console.error("Error clearing route", err.stack || err);
console.error("Error clearing route", err);
});
}
for (const routeId of Object.keys(this.routes)) {
this.database.routes.deleteRoute(this.routes[routeId].id).catch((err) => {
console.error("Error clearing route", err.stack || err);
console.error("Error clearing route", err);
});
}
}
@ -196,9 +196,9 @@ class SocketConnection {
if(admin)
pad = { ...admin, writable: Writable.ADMIN };
else if(write)
pad = { ...write, writable: Writable.WRITE, adminId: undefined };
pad = { ...write, writable: Writable.WRITE, adminId: null };
else if(read)
pad = { ...read, writable: Writable.READ, writeId: undefined, adminId: undefined };
pad = { ...read, writable: Writable.READ, writeId: null, adminId: null };
else {
this.padId = undefined;
throw new Error("This pad does not exist");

Wyświetl plik

@ -115,14 +115,14 @@ export function cruValidator<
...declaration.exceptRead,
...declaration.exceptUpdate
}),
update: {
update: z.object({
...declaration.all,
...Object.fromEntries(Object.entries(declaration.allPartialCreate ?? {}).map(([k, v]) => [k, v.optional()])),
...Object.fromEntries(Object.entries(declaration.allPartialUpdate ?? {}).map(([k, v]) => [k, v.optional()])),
...declaration.onlyUpdate,
...declaration.exceptRead,
...declaration.exceptCreate
}
})
} as any;
}

Wyświetl plik

@ -5,8 +5,8 @@ import { ID } from "./base.js";
import { Marker } from "./marker.js";
import { Line } from "./line.js";
export type MarkerFeature = Feature<Point, Omit<Marker, "id" | "padId" | "lat" | "lon">>;
export type LineFeature = Feature<LineString, Omit<Line, "id" | "padId" | "top" | "left" | "right" | "bottom">>;
export type MarkerFeature = Feature<Point, Omit<Marker, "id" | "padId" | "lat" | "lon" | "ele">>;
export type LineFeature = Feature<LineString, Omit<Line, "id" | "padId" | "top" | "left" | "right" | "bottom" | "extraInfo" | "ascent" | "descent">>;
export interface GeoJsonExtensions {
name: string;

Wyświetl plik

@ -7,8 +7,10 @@ export type ExtraInfo = z.infer<typeof extraInfoValidator>;
export const trackPointValidator = cruValidator({
all: {
...pointValidator.shape,
ele: z.number().optional()
...pointValidator.shape
},
allPartialCreate: {
ele: z.number().or(z.null())
},
onlyRead: {
idx: z.number(),
@ -26,9 +28,9 @@ export const lineValidator = cruValidator({
},
allPartialUpdate: {
routePoints: z.array(pointValidator).min(2),
name: z.string().optional(),
name: z.string(),
typeId: idValidator,
extraInfo: extraInfoValidator.optional()
extraInfo: extraInfoValidator.or(z.null())
},
exceptCreate: {
id: idValidator
@ -36,12 +38,12 @@ export const lineValidator = cruValidator({
onlyRead: {
...bboxValidator.shape,
distance: z.number(),
ascent: z.number().optional(),
descent: z.number().optional(),
time: z.number().optional(),
ascent: z.number().or(z.null()),
descent: z.number().or(z.null()),
time: z.number().or(z.null()),
padId: padIdValidator
},
onlyCreate: {
exceptRead: {
trackPoints: z.array(trackPointValidator.create).optional()
}
});

Wyświetl plik

@ -4,16 +4,16 @@ import * as z from "zod";
export const markerValidator = cruValidator({
allPartialCreate: {
name: z.string(),
symbol: symbolValidator.or(z.null()),
shape: shapeValidator.or(z.null()),
ele: z.number().or(z.null()),
colour: colourValidator,
size: sizeValidator,
data: z.record(z.string())
},
allPartialUpdate: {
...pointValidator.shape,
name: z.string().optional(),
symbol: symbolValidator.optional(),
shape: shapeValidator.optional(),
ele: z.number().optional(),
typeId: idValidator
},
exceptCreate: {

Wyświetl plik

@ -23,11 +23,11 @@ export const padDataValidator = cruValidator({
},
onlyRead: {
writable: writableValidator,
defaultView: viewValidator.read.optional()
defaultView: viewValidator.read.or(z.null())
},
exceptCreate: {
writeId: padIdValidator.optional(),
adminId: padIdValidator.optional()
writeId: padIdValidator.or(z.null()),
adminId: padIdValidator.or(z.null())
},
onlyCreate: {
writeId: padIdValidator,

Wyświetl plik

@ -55,21 +55,24 @@ export type Field<Mode extends CRU = CRU.READ> = CRUType<Mode, typeof fieldValid
export type FieldUpdate = Field<CRU.UPDATE>;
export const typeValidator = cruValidator({
allPartialCreate: {
defaultColour: colourValidator.or(z.null()),
colourFixed: z.boolean().or(z.null()),
defaultSize: sizeValidator.or(z.null()),
sizeFixed: z.boolean().or(z.null()),
defaultSymbol: symbolValidator.or(z.null()),
symbolFixed: z.boolean().or(z.null()),
defaultShape: shapeValidator.or(z.null()),
shapeFixed: z.boolean().or(z.null()),
defaultWidth: widthValidator.or(z.null()),
widthFixed: z.boolean().or(z.null()),
defaultMode: routeModeValidator.or(z.null()),
modeFixed: z.boolean().or(z.null()),
showInLegend: z.boolean().or(z.null()),
},
allPartialUpdate: {
name: z.string(),
defaultColour: colourValidator.optional(),
colourFixed: z.boolean().optional(),
defaultSize: sizeValidator.optional(),
sizeFixed: z.boolean().optional(),
defaultSymbol: symbolValidator.optional(),
symbolFixed: z.boolean().optional(),
defaultShape: shapeValidator.optional(),
shapeFixed: z.boolean().optional(),
defaultWidth: widthValidator.optional(),
widthFixed: z.boolean().optional(),
defaultMode: routeModeValidator.optional(),
modeFixed: z.boolean().optional(),
showInLegend: z.boolean().optional(),
name: z.string()
},
exceptCreate: {

Wyświetl plik

@ -3,12 +3,15 @@ import { CRU, CRUType, cruValidator } from "./cru.js";
import * as z from "zod";
export const viewValidator = cruValidator({
allPartialCreate: {
filter: z.string().or(z.null())
},
allPartialUpdate: {
...bboxValidator.shape,
name: z.string(),
baseLayer: layerValidator,
layers: z.array(layerValidator),
filter: z.string().optional()
layers: z.array(layerValidator)
},
exceptCreate: {

Wyświetl plik

@ -39,6 +39,7 @@
"jsdom": "^22.1.0",
"linkify-string": "^4.1.1",
"linkifyjs": "^4.1.1",
"lodash-es": "^4.17.21",
"marked": "^9.1.0"
},
"devDependencies": {

Wyświetl plik

@ -1,6 +1,7 @@
import { compileExpression as filtrexCompileExpression } from "filtrex";
import { clone, flattenObject, getProperty, quoteRegExp } from "./utils.js";
import { flattenObject, getProperty, quoteRegExp } from "./utils.js";
import { ID, Marker, Line, Type, Field, CRU } from "facilmap-types";
import { cloneDeep } from "lodash-es";
export type FilterFunc = (obj: Marker<CRU> | Line<CRU>, type: Type) => boolean;
@ -109,7 +110,7 @@ export function makeTypeFilter(previousFilter: string = "", typeId: ID, filtered
}
export function prepareObject<T extends Marker<CRU> | Line<CRU>>(obj: T, type: Type): T & { type?: Type["type"] } {
obj = clone(obj);
obj = cloneDeep(obj);
for (const field of type.fields) {
if (Object.getPrototypeOf(obj.data)?.set)

Wyświetl plik

@ -100,29 +100,6 @@ export function encodeQueryString(obj: Record<string, string>): string {
return pairs.join("&");
}
function applyPrototypes(source: any, target: any): void {
if (typeof source === 'object' && source != null) {
if (Array.isArray(source)) {
for (let i = 0; i < source.length; i++)
applyPrototypes(source[i], target[i]);
} else {
Object.setPrototypeOf(target, Object.getPrototypeOf(source));
for (const key of Object.keys(source))
applyPrototypes(source[key], target[key]);
}
}
}
export function clone<T>(obj: T): T {
if (typeof obj !== "object" || !obj)
return obj;
const result = JSON.parse(JSON.stringify(obj));
applyPrototypes(obj, result);
return result;
}
export function* numberKeys(obj: Record<number, any>): Generator<number> {
for (const idx of Object.keys(obj)) {
// https://stackoverflow.com/a/175787/242365

Wyświetl plik

@ -46,6 +46,18 @@ __metadata:
languageName: node
linkType: hard
"@ckpack/vue-color@npm:^1.5.0":
version: 1.5.0
resolution: "@ckpack/vue-color@npm:1.5.0"
dependencies:
"@ctrl/tinycolor": ^3.6.0
material-colors: ^1.2.6
peerDependencies:
vue: ^3.2.0
checksum: 8411b9fe0080378c347ea0421757a8652b52e7514596b227d18e07ff1b55eca061b008b8740fddbe9e6848f84fe7196e6abfe4c1a4d2929eb2b3224bcd62e078
languageName: node
linkType: hard
"@cspotcode/source-map-support@npm:^0.8.0":
version: 0.8.1
resolution: "@cspotcode/source-map-support@npm:0.8.1"
@ -55,6 +67,13 @@ __metadata:
languageName: node
linkType: hard
"@ctrl/tinycolor@npm:^3.6.0":
version: 3.6.1
resolution: "@ctrl/tinycolor@npm:3.6.1"
checksum: cefec6fcaaa3eb8ddf193f981e097dccf63b97b93b1e861cb18c645654824c831a568f444996e15ee509f255658ed82fba11c5365494a6e25b9b12ac454099e0
languageName: node
linkType: hard
"@digitak/esrun@npm:^3.2.25":
version: 3.2.25
resolution: "@digitak/esrun@npm:3.2.25"
@ -2309,13 +2328,6 @@ __metadata:
languageName: node
linkType: hard
"clamp@npm:^1.0.1":
version: 1.0.1
resolution: "clamp@npm:1.0.1"
checksum: 799bd7083736eb975cd4a9a7e8f1a1e38cc3cb6be0384f9732c1da263accb3205385e5c2880e661a0d5a74e0066bfbf8fcd17dd2f509595ce52dd04c84522833
languageName: node
linkType: hard
"clean-css@npm:^4.2.1":
version: 4.2.4
resolution: "clean-css@npm:4.2.4"
@ -3731,6 +3743,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "facilmap-frontend@workspace:frontend"
dependencies:
"@ckpack/vue-color": ^1.5.0
"@tmcw/togeojson": ^5.8.1
"@types/bootstrap": ^5.2.8
"@types/decode-uri-component": ^0.2.0
@ -3793,7 +3806,6 @@ __metadata:
vite-plugin-dts: ^3.6.0
vitest: ^0.34.6
vue: ^3.3.4
vue-color: ^2.8.1
vue-template-compiler: ^2.7.14
vue-template-loader: ^1.1.0
vuedraggable: next
@ -3940,6 +3952,7 @@ __metadata:
jsdom: ^22.1.0
linkify-string: ^4.1.1
linkifyjs: ^4.1.1
lodash-es: ^4.17.21
marked: ^9.1.0
rimraf: ^5.0.5
rollup-plugin-auto-external: ^2.0.0
@ -5498,13 +5511,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.throttle@npm:^4.0.0":
version: 4.1.1
resolution: "lodash.throttle@npm:4.1.1"
checksum: 129c0a28cee48b348aef146f638ef8a8b197944d4e9ec26c1890c19d9bf5a5690fe11b655c77a4551268819b32d27f4206343e30c78961f60b561b8608c8c805
languageName: node
linkType: hard
"lodash@npm:^4.17.21, lodash@npm:~4.17.15":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
@ -5639,7 +5645,7 @@ __metadata:
languageName: node
linkType: hard
"material-colors@npm:^1.0.0":
"material-colors@npm:^1.2.6":
version: 1.2.6
resolution: "material-colors@npm:1.2.6"
checksum: 72d005ccccb82bab68eef3cd757e802668634fc86976dedb9fc564ce994f2d3258273766b7efecb7404a0031969e2d72201a1b74169763f0a53c0dd8d649209f
@ -7965,13 +7971,6 @@ __metadata:
languageName: node
linkType: hard
"tinycolor2@npm:^1.1.2":
version: 1.6.0
resolution: "tinycolor2@npm:1.6.0"
checksum: 6df4d07fceeedc0a878d7bac47e2cd47c1ceeb1078340a9eb8a295bc0651e17c750f73d47b3028d829f30b85c15e0572c0fd4142083e4c21a30a597e47f47230
languageName: node
linkType: hard
"tinypool@npm:^0.7.0":
version: 0.7.0
resolution: "tinypool@npm:0.7.0"
@ -8584,18 +8583,6 @@ __metadata:
languageName: node
linkType: hard
"vue-color@npm:^2.8.1":
version: 2.8.1
resolution: "vue-color@npm:2.8.1"
dependencies:
clamp: ^1.0.1
lodash.throttle: ^4.0.0
material-colors: ^1.0.0
tinycolor2: ^1.1.2
checksum: 3c9e5f42f304f5d333a9ac9916dc0cd796d34a98d0fd8b2fc3eac93eb56eef949f6aa7eef6ac5ec609eb0a038b2a2b3b73e83d29567dc99b5e0df8ea9fe64659
languageName: node
linkType: hard
"vue-eslint-parser@npm:^9.3.1":
version: 9.3.2
resolution: "vue-eslint-parser@npm:9.3.2"