First commit.

This commit is contained in:
Lasse S. Haslev
2020-10-25 18:34:21 +01:00
commit 97065c0377
22 changed files with 26186 additions and 0 deletions

10
.gitignore vendored Executable file
View File

@@ -0,0 +1,10 @@
/.idea
/vendor
/node_modules
package-lock.json
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db

29
composer.json Executable file
View File

@@ -0,0 +1,29 @@
{
"name": "lassehaslev/nova-map-fields",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": ">=7.1.0"
},
"autoload": {
"psr-4": {
"Lassehaslev\\NovaMapFields\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Lassehaslev\\NovaMapFields\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

0
dist/css/field.css vendored Executable file
View File

BIN
dist/images/vendor/leaflet/dist/layers-2x.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
dist/images/vendor/leaflet/dist/layers.png vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

25553
dist/js/field.js vendored Executable file

File diff suppressed because one or more lines are too long

4
dist/mix-manifest.json vendored Executable file
View File

@@ -0,0 +1,4 @@
{
"/js/field.js": "/js/field.js",
"/css/field.css": "/css/field.css"
}

21
package.json Executable file
View File

@@ -0,0 +1,21 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.0.0",
"laravel-mix": "^1.0",
"laravel-nova": "^1.0"
},
"dependencies": {
"vue": "^2.5.0",
"vue2-leaflet": "^1.0.2"
}
}

View File

@@ -0,0 +1,15 @@
<template>
<div>
<panel-item :field="field">
<template slot="value">
<component :is="'field-' + field.map.type" :field="field" v-model="field.value"></component>
</template>
</panel-item>
</div>
</template>
<script>
export default {
props: ['resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,59 @@
<template>
<div :style="{'height': field.map.height}">
<l-map
style="z-index:0"
:zoom="field.map.zoom"
:center="center"
:bounds="bounds"
@click="onMapClick"
>
<l-tile-layer
:url="url"
:attribution="attribution"/>
<slot></slot>
</l-map>
</div>
</template>
<script>
import {LMap, LTileLayer} from 'vue2-leaflet'
export default {
props: {
field: {
type: Object,
requred: true,
},
center: {
type: Object,
default: null,
},
bounds: {
type: Object,
default: null,
},
},
data() {
return {
attribution: '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
}
},
methods: {
onMapClick(evt) {
this.$emit('click', evt);
}
},
components: {
LMap,
LTileLayer,
}
}
</script>
<style scoped>
@import "../../../node_modules/leaflet/dist/leaflet.css"
</style>

View File

@@ -0,0 +1,40 @@
<template>
<default-field :field="field" :errors="errors">
<template slot="field">
<component v-if="value" :is="'field-' + field.map.type" :field="field" :edit="true" v-model="value"></component>
</template>
</default-field>
</template>
<script>
import { FormField, HandlesValidationErrors } from 'laravel-nova'
export default {
mixins: [FormField, HandlesValidationErrors],
props: ['resourceName', 'resourceId', 'field'],
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.field.value || {}
},
/**
* Fill the given FormData object with the field's internal value.
*/
fill(formData) {
formData.append(this.field.attribute, JSON.stringify(this.value));
},
/**
* Update the field's internal value.
*/
handleChange(value) {
this.value = value
},
},
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<span>{{ field.value }}</span>
</template>
<script>
export default {
props: ['resourceName', 'field'],
}
</script>

View File

@@ -0,0 +1,80 @@
<template>
<field-map :center="position" :field="field">
<l-marker :lat-lng="position" :draggable="edit" @dragend="onDragEnd"/>
</field-map>
</template>
<script>
import {LMarker} from 'vue2-leaflet'
export default {
props: {
value: {
type: Object,
default: null,
},
field: {
type: Object,
required: true,
},
edit: {
type: Boolean,
default: false,
}
},
computed: {
position() {
return L.latLng(this.value.lat || this.field.map.center.latitude, this.value.lng || this.field.map.center.longitude);
}
},
mounted() {
this.registerWatchers();
},
methods: {
onDragEnd(evt) {
let latLng = evt.target.getLatLng();
this.$emit('input', {
lat: latLng.lat,
lng: latLng.lng,
});
},
registerWatchers() {
this.getGlobalFieldComponents().forEach(component => {
if ([this.field.latitudeField, this.field.longitudeField].includes(component.field.attribute)) {
component.$watch('value', (value) => {
this.value[component.field.attribute] = value;
}, {immediate: true})
}
});
},
getGlobalFieldComponents(components = null) {
if (! components) {
components = this.$root.$children;
}
let returnArray = [];
components.forEach(component => {
if (component.field) {
return returnArray.push(component);
}
returnArray = returnArray.concat(this.getGlobalFieldComponents(component.$children));
});
return returnArray;
},
},
components: {
LMarker,
}
}
</script>

View File

@@ -0,0 +1,87 @@
<template>
<div @keydown.backspace="removeLastMarker">
<field-map :bounds="bounds" :center="center" :field="field" @click="createMarker">
<l-marker v-for="marker in markers" :lat-lng="marker" :draggable="edit" @dragend="triggerChange" />
<l-polyline :lat-lngs="value" :visible="true" />
</field-map>
</div>
</template>
<script>
import {LMarker, LPolyline} from 'vue2-leaflet'
export default {
props: {
value: {
type: Object,
default: null,
},
field: {
type: Object,
required: true,
},
edit: {
type: Boolean,
default: false,
}
},
data() {
return {
startBounds: undefined,
}
},
computed: {
markers() {
if (!this.value[0]) {
return [];
}
return this.value;
},
center() {
if (!this.markers.length) {
return {
lat: this.field.map.center.latitude,
lng: this.field.map.center.longitude,
};
}
return this.bounds.getCenter();
},
bounds() {
if (this.startBounds !== undefined) {
return this.startBounds;
}
if (!this.markers.length) {
return null;
}
return this.startBounds = new L.LatLngBounds(this.markers);
},
},
methods: {
triggerChange() {
this.$emit('input', this.markers);
},
removeLastMarker() {
this.markers.splice(-1,1);
this.triggerChange();
},
createMarker(evt) {
this.markers.push(evt.latlng);
this.triggerChange();
}
},
components: {
LMarker,
LPolyline,
}
}
</script>

12
resources/js/field.js Executable file
View File

@@ -0,0 +1,12 @@
Nova.booting((Vue, router) => {
Vue.component('index-nova-map-fields', require('./components/IndexField'));
Vue.component('detail-nova-map-fields', require('./components/DetailField'));
Vue.component('form-nova-map-fields', require('./components/FormField'));
Vue.component('field-map', require('./components/FieldMap'));
Vue.component('field-marker', require('./components/fields/Marker'));
Vue.component('field-polyline', require('./components/fields/Polyline'));
// Config leaflet images to load images from CDN
L.Icon.Default.imagePath = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.4/images/';
})

1
resources/sass/field.scss Executable file
View File

@@ -0,0 +1 @@
// Nova Tool CSS

28
src/FieldServiceProvider.php Executable file
View File

@@ -0,0 +1,28 @@
<?php
namespace Lassehaslev\NovaMapFields;
use Laravel\Nova\Nova;
use Laravel\Nova\Events\ServingNova;
use Illuminate\Support\ServiceProvider;
class FieldServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('nova-map-fields', __DIR__.'/../dist/js/field.js');
Nova::style('nova-map-fields', __DIR__.'/../dist/css/field.css');
});
}
/**
* Register any application services.
*/
public function register()
{
}
}

109
src/MapField.php Executable file
View File

@@ -0,0 +1,109 @@
<?php
namespace Lassehaslev\NovaMapFields;
use Laravel\Nova\Fields\Field;
use Illuminate\Support\Str;
abstract class MapField extends Field
{
/**
* The field's component.
*
* @var string
*/
public $component = 'nova-map-fields';
/**
* Hide from index field.
*
* @var bool
*/
public $showOnIndex = false;
/**
* Create a new field.
* And set default properties.
*
* @param string $name
* @param string|null $attribute
* @param mixed|null $resolveCallback
*/
public function __construct($name, $attribute = null, $resolveCallback = null)
{
$this->setHeight('250px');
$this->setCenter(0, 0);
$this->setZoom(13);
$this->withMapMeta('type', $this->getMapFieldType());
parent::__construct($name, $attribute = null, $resolveCallback);
}
/**
* Set the height for the map.
*
* @param mixed $height
*
* @return $this
*/
public function setHeight($height)
{
return $this->withMapMeta('height', $height);
}
/**
* Set the zoom for the map.
*
* @param mixed $level
*
* @return $this
*/
public function setZoom($level)
{
return $this->withMapMeta('zoom', $level);
}
/**
* Set the position for the map.
*
* @param mixed $latitude
* @param mixed $longitude
*
* @return $this
*/
public function setCenter($latitude, $longitude)
{
return $this->withMapMeta('center', [
'latitude' => $latitude,
'longitude' => $longitude,
]);
}
/**
* Add meta data for the map.
*
* @param mixed $key
* @param mixed $value
*/
protected function withMapMeta($key, $value)
{
$existingMapMeta = $this->meta['map'] ?? [];
return $this->withMeta([
'map' => array_merge($existingMapMeta, [
$key => $value,
]),
]);
}
/**
* Get component name.
*
* @return string
*/
public function getMapFieldType()
{
return Str::kebab(class_basename($this));
}
}

85
src/Marker.php Executable file
View File

@@ -0,0 +1,85 @@
<?php
namespace Lassehaslev\NovaMapFields;
use Laravel\Nova\Http\Requests\NovaRequest;
class Marker extends MapField
{
/**
* Create a new field.
* And set default properties.
*
* @param string $name
* @param string|null $attribute
* @param mixed|null $resolveCallback
*/
public function __construct($name, $attribute = null, $resolveCallback = null)
{
$this->help('Drag the marker to change the point.');
parent::__construct($name, $attribute = null, $resolveCallback);
}
/**
* Set the latutude field.
*
* @param mixed $fieldName
*
* @return $this
*/
public function setLatitude($fieldName)
{
return $this->withMeta([
'latitudeField' => $fieldName,
]);
}
/**
* Set the longitude field.
*
* @param mixed $fieldName
*
* @return $this
*/
public function setLongitude($fieldName)
{
return $this->withMeta([
'longitudeField' => $fieldName,
]);
}
/**
* Resolve the attribute before sending to frontend.
*
* @param mixed $resource
* @param mixed|null $attribute
*
* @return array
*/
public function resolveAttribute($resource, $attribute = null)
{
return [
'lat' => $resource->{$this->meta['latitudeField']},
'lng' => $resource->{$this->meta['longitudeField']},
];
}
/**
* Hydrate the given attribute on the model based on the incoming request.
*
* @param \Laravel\Nova\Http\Requests\NovaRequest $request
* @param string $requestAttribute
* @param object $model
* @param string $attribute
*/
protected function fillAttributeFromRequest(NovaRequest $request, $requestAttribute, $model, $attribute)
{
if ($request->exists($requestAttribute)) {
$latLng = json_decode($request[$requestAttribute]);
$model->{$this->meta['latitudeField']} = $latLng->lat;
$model->{$this->meta['longitudeField']} = $latLng->lng;
}
}
}

39
src/Polyline.php Executable file
View File

@@ -0,0 +1,39 @@
<?php
namespace Lassehaslev\NovaMapFields;
class Polyline extends MapField
{
/**
* Create a new field.
* And set default properties.
*
* @param string $name
* @param string|null $attribute
* @param mixed|null $resolveCallback
*/
public function __construct($name, $attribute = null, $resolveCallback = null)
{
$this->help('Click on the map to create a new point. Drag a marker to change the point. When the map is selected, you can press [backspace] to remove markers.');
parent::__construct($name, $attribute = null, $resolveCallback);
}
/**
* Resolve the attribute before sending to frontend.
*
* @param mixed $resource
* @param mixed|null $attribute
*
* @return array
*/
public function resolveAttribute($resource, $attribute = null)
{
return json_decode($resource->{$attribute});
return [
'lat' => $resource->{$this->meta['latitudeField']},
'lng' => $resource->{$this->meta['longitudeField']},
];
}
}

5
webpack.mix.js Executable file
View File

@@ -0,0 +1,5 @@
let mix = require('laravel-mix')
mix.setPublicPath('dist')
.js('resources/js/field.js', 'js')
.sass('resources/sass/field.scss', 'css')