kopia lustrzana https://github.com/FacilMap/facilmap
pull/256/head
rodzic
b8583ce570
commit
31ca895acc
|
@ -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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()"
|
||||
>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="btn-group">
|
||||
<div class="btn-toolbar">
|
||||
<ZoomToObjectButton
|
||||
v-if="zoomDestination"
|
||||
label="selection"
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 }">
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import "./bootstrap.scss";
|
||||
import "./styles.scss";
|
||||
|
||||
import { registerDeobfuscationHandlers } from "../utils/obfuscate";
|
||||
|
||||
registerDeobfuscationHandlers();
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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>;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
#b-toaster-top-right {
|
||||
z-index: 10002;
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
57
yarn.lock
57
yarn.lock
|
@ -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"
|
||||
|
|
Ładowanie…
Reference in New Issue