Merge pull request #4856 from Budibase/feature/map-component

Feature/map component
This commit is contained in:
deanhannigan 2022-03-25 11:57:34 +00:00 committed by GitHub
commit f11ee00acd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 670 additions and 3 deletions

View File

@ -28,5 +28,8 @@
],
"rules": {
"no-self-assign": "off"
},
"globals": {
"GeolocationPositionError": true
}
}

View File

@ -82,7 +82,8 @@
"link",
"icon",
"embed",
"markdownviewer"
"markdownviewer",
"embeddedmap"
]
}
]

View File

@ -2524,6 +2524,82 @@
}
]
},
"embeddedmap": {
"name": "Embedded Map",
"icon": "Location",
"styles": ["size"],
"editable": true,
"draggable": false,
"illegalChildren": [
"section"
],
"settings": [
{
"type": "dataProvider",
"label": "Provider",
"key": "dataProvider"
},
{
"type": "boolean",
"label": "Enable Fullscreen",
"key": "fullScreenEnabled",
"defaultValue": true
},
{
"type": "boolean",
"label": "Enable Location",
"key": "locationEnabled",
"defaultValue": true
},
{
"type": "boolean",
"label": "Enable Zoom",
"key": "zoomEnabled",
"defaultValue": true
},
{
"type": "number",
"label": "Zoom Level (0-100)",
"key": "zoomLevel",
"defaultValue": 72,
"max" : 100,
"min" : 0
},
{
"type": "field",
"label": "Latitude Key",
"key": "latitudeKey"
},
{
"type": "field",
"label": "Longitude Key",
"key": "longitudeKey"
},
{
"type": "field",
"label": "Title Key",
"key": "titleKey"
},
{
"type": "text",
"label": "Tile URL",
"key": "tileURL",
"defaultValue": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
},
{
"type": "text",
"label": "Default Location (lat,lng)",
"key": "defaultLocation",
"defaultValue": "51.5072,-0.1276"
},
{
"type": "text",
"label": "Map Attribution",
"key": "mapAttribution",
"defaultValue": "OpenStreetMap contributors"
}
]
},
"attachmentfield": {
"name": "Attachment",
"icon": "Attach",

View File

@ -33,8 +33,11 @@
"apexcharts": "^3.22.1",
"dayjs": "^1.10.5",
"downloadjs": "1.4.7",
"leaflet": "^1.7.1",
"regexparam": "^1.3.0",
"rollup-plugin-polyfill-node": "^0.8.0",
"sanitize-html": "^2.7.0",
"screenfull": "^6.0.1",
"shortid": "^2.2.15",
"svelte": "^3.38.2",
"svelte-apexcharts": "^1.0.2",

View File

@ -103,7 +103,12 @@
($builderStore.previewType === "layout" || insideScreenslot) &&
!isBlock
$: editing = editable && selected && $builderStore.editMode
$: draggable = !inDragPath && interactive && !isLayout && !isScreen
$: draggable =
!inDragPath &&
interactive &&
!isLayout &&
!isScreen &&
definition?.draggable !== false
$: droppable = interactive && !isLayout && !isScreen
// Empty components are those which accept children but do not have any.

View File

@ -0,0 +1,297 @@
<script>
import L from "leaflet"
import sanitizeHtml from "sanitize-html"
import "leaflet/dist/leaflet.css"
import { Helpers } from "@budibase/bbui"
import { getContext } from "svelte"
import {
FullScreenControl,
LocationControl,
initMapControls,
} from "./EmbeddedMapControls"
initMapControls()
export let dataProvider
export let error
export let zoomLevel
export let zoomEnabled = true
export let latitudeKey = null
export let longitudeKey = null
export let titleKey = null
export let fullScreenEnabled = true
export let locationEnabled = true
export let defaultLocation
export let tileURL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
export let mapAttribution
const { styleable, notificationStore } = getContext("sdk")
const component = getContext("component")
const embeddedMapId = `${Helpers.uuid()}-wrapper`
let cachedDeviceCoordinates
const fallbackCoordinates = [51.5072, -0.1276] //London
let mapInstance
let mapMarkerGroup = new L.FeatureGroup()
let mapMarkers = []
let minZoomLevel = 0
let maxZoomLevel = 18
let adjustedZoomLevel = !Number.isInteger(zoomLevel)
? 72
: Math.round(zoomLevel * (maxZoomLevel / 100))
$: zoomControlUpdated(mapInstance, zoomEnabled)
$: locationControlUpdated(mapInstance, locationEnabled)
$: fullScreenControlUpdated(mapInstance, fullScreenEnabled)
$: updateMapDimensions(
mapInstance,
$component.styles.normal.width,
$component.styles.normal.height
)
$: addMapMarkers(
mapInstance,
dataProvider?.rows,
latitudeKey,
longitudeKey,
titleKey
)
$: if (typeof mapInstance === "object" && mapMarkers.length > 0) {
mapInstance.setZoom(0)
mapInstance.fitBounds(mapMarkerGroup.getBounds(), {
paddingTopLeft: [0, 24],
})
}
const updateMapDimensions = mapInstance => {
if (typeof mapInstance !== "object") {
return
}
mapInstance.invalidateSize()
}
let isValidLatitude = value => {
return !isNaN(value) && value > -90 && value < 90
}
let isValidLongitude = value => {
return !isNaN(value) && value > -180 && value < 180
}
const parseDefaultLocation = defaultLocation => {
if (typeof defaultLocation !== "string") {
return fallbackCoordinates
}
let defaultLocationParts = defaultLocation.split(",")
if (defaultLocationParts.length !== 2) {
return fallbackCoordinates
}
let parsedDefaultLatitude = parseFloat(defaultLocationParts[0].trim())
let parsedDefaultLongitude = parseFloat(defaultLocationParts[1].trim())
return isValidLatitude(parsedDefaultLatitude) === true &&
isValidLongitude(parsedDefaultLongitude) === true
? [parsedDefaultLatitude, parsedDefaultLongitude]
: fallbackCoordinates
}
$: defaultCoordinates =
mapMarkers.length > 0
? parseDefaultLocation(defaultLocation)
: fallbackCoordinates
// Map Button Controls
let locationControl = new LocationControl({
position: "bottomright",
onLocationFail: err => {
if (err.code === GeolocationPositionError.PERMISSION_DENIED) {
notificationStore.actions.error(
"Location requests not permitted. Ensure location is enabled"
)
} else if (err.code === GeolocationPositionError.POSITION_UNAVAILABLE) {
notificationStore.actions.warning(
"Location could not be retrieved. Try again"
)
} else if (err.code === GeolocationPositionError.TIMEOUT) {
notificationStore.actions.warning(
"Location request timed out. Try again"
)
} else {
notificationStore.actions.error("Unknown location error")
}
},
onLocationSuccess: pos => {
cachedDeviceCoordinates = pos
if (typeof mapInstance === "object") {
mapInstance.setView(cachedDeviceCoordinates, 15)
}
},
})
let fullScreenControl = new FullScreenControl({
position: "topright",
})
let zoomControl = L.control.zoom({
position: "bottomright",
})
const locationControlUpdated = (mapInstance, locationEnabled) => {
if (typeof mapInstance !== "object") {
return
}
if (locationEnabled) {
locationControl.addTo(mapInstance)
} else {
mapInstance.removeControl(locationControl)
}
}
const fullScreenControlUpdated = (mapInstance, fullScreenEnabled) => {
if (typeof mapInstance !== "object") {
return
}
if (fullScreenEnabled) {
fullScreenControl.addTo(mapInstance)
} else {
mapInstance.removeControl(fullScreenControl)
}
}
const zoomControlUpdated = (mapInstance, zoomEnabled) => {
if (typeof mapInstance !== "object") {
return
}
if (zoomEnabled) {
zoomControl.addTo(mapInstance)
mapInstance.scrollWheelZoom.enable()
} else {
mapInstance.removeControl(zoomControl)
mapInstance.scrollWheelZoom.disable()
}
}
//Map icon and marker configuration
const mapIconMarkup =
'<div><svg width="26px" height="26px" class="spectrum-Icon" focusable="false" stroke="#b12b27" stroke-width="1%">' +
'<use xlink:href="#spectrum-icon-18-Location" /></svg></div>'
const mapIcon = L.divIcon({
html: mapIconMarkup,
className: "embedded-map-marker",
iconSize: [26, 26],
iconAnchor: [13, 26],
popupAnchor: [0, -13],
})
const mapMarkerOptions = {
icon: mapIcon,
draggable: false,
alt: "Location Marker",
}
let mapOptions = {
fullScreen: false,
zoomControl: false,
scrollWheelZoom: zoomEnabled,
minZoomLevel,
maxZoomLevel,
}
const addMapMarkers = (mapInstance, rows, latKey, lngKey, titleKey) => {
if (typeof mapInstance !== "object" || !rows || !latKey || !lngKey) {
return
}
mapMarkerGroup.clearLayers()
const validRows = rows.filter(row => {
return isValidLatitude(row[latKey]) && isValidLongitude(row[lngKey])
})
validRows.forEach(row => {
let markerCoords = [row[latKey], row[lngKey]]
let marker = L.marker(markerCoords, mapMarkerOptions).addTo(mapInstance)
let markerContent = generateMarkerPopupContent(
row[latKey],
row[lngKey],
row[titleKey]
)
marker.bindPopup(markerContent).addTo(mapMarkerGroup)
//https://github.com/Leaflet/Leaflet/issues/7331
marker.on("click", function () {
this.openPopup()
})
mapMarkers = [...mapMarkers, marker]
})
}
const generateMarkerPopupContent = (latitude, longitude, text) => {
return text || latitude + "," + longitude
}
const initMap = () => {
const initCoords = defaultCoordinates
mapInstance = L.map(embeddedMapId, mapOptions)
mapMarkerGroup.addTo(mapInstance)
const cleanAttribution = sanitizeHtml(mapAttribution, {
allowedTags: ["a"],
allowedAttributes: {
a: ["href", "target"],
},
})
L.tileLayer(tileURL, {
attribution: "&copy; " + cleanAttribution,
zoom: adjustedZoomLevel,
}).addTo(mapInstance)
//Initialise the map
mapInstance.setView(initCoords, adjustedZoomLevel)
}
const mapAction = () => {
initMap()
return {
destroy() {
mapInstance.remove()
mapInstance = undefined
},
}
}
</script>
<div class="embedded-map-wrapper map-default" use:styleable={$component.styles}>
{#if error}
<div>{error}</div>
{/if}
<div id={embeddedMapId} class="embedded embedded-map" use:mapAction />
</div>
<style>
.embedded-map-wrapper {
background-color: #f1f1f1;
}
.map-default {
min-height: 180px;
min-width: 200px;
}
.embedded-map :global(.leaflet-top),
.embedded-map :global(.leaflet-bottom) {
z-index: 998;
}
.embedded-map :global(.embedded-map-marker) {
color: #ee3b35;
}
.embedded-map :global(.embedded-map-control) {
font-size: 22px;
}
.embedded-map {
height: 100%;
width: 100%;
}
</style>

View File

@ -0,0 +1,192 @@
import L from "leaflet"
import screenfull from "screenfull"
const createButton = function (html, title, className, container, fn) {
let link = L.DomUtil.create("a", className, container)
link.innerHTML = html
link.href = "#"
link.title = title
link.setAttribute("role", "button")
link.setAttribute("aria-label", title)
L.DomEvent.disableClickPropagation(link)
L.DomEvent.on(link, "click", L.DomEvent.stop)
L.DomEvent.on(link, "click", fn, this)
L.DomEvent.on(link, "click", this._refocusOnMap, this)
return link
}
// Full Screen Control
const FullScreenControl = L.Control.extend({
options: {
position: "topright",
fullScreenContent:
'<span class="embedded-map-control embedded-map-location-icon">' +
'<svg width="16px" height="16px" class="spectrum-Icon" focusable="false">' +
'<use xlink:href="#spectrum-icon-18-FullScreen" /></svg><span>',
fullScreenTitle: "Enter Fullscreen",
},
onAdd: function () {
var fullScreenClassName = "leaflet-control-fullscreen",
container = L.DomUtil.create("div", fullScreenClassName + " leaflet-bar"),
options = this.options
this._fullScreenButton = this._createButton(
options.fullScreenContent,
options.fullScreenTitle,
"map-fullscreen",
container,
this._fullScreen
)
return container
},
_fullScreen: function () {
var map = this._map
if (screenfull.isEnabled) {
screenfull.toggle(map.getContainer())
}
},
_createButton: createButton,
})
const initFullScreenControl = () => {
L.Map.mergeOptions({
fullScreen: false,
})
L.Map.addInitHook(function () {
if (this.options.fullScreen) {
this.fullScreenControl = new FullScreenControl()
this.addControl(this.fullScreenControl)
} else {
this.fullScreenControl = null
}
})
}
// Location Control
const LocationControl = L.Control.extend({
options: {
position: "topright",
locationContent:
'<span class="embedded-map-control embedded-map-location-icon">' +
'<svg width="16px" height="16px" class="spectrum-Icon" focusable="false">' +
'<use xlink:href="#spectrum-icon-18-Campaign" /></svg><span>',
locationTitle: "Show Your Location",
},
onAdd: function () {
var locationClassName = "leaflet-control-location",
container = L.DomUtil.create("div", locationClassName + " leaflet-bar"),
options = this.options
this._locationButton = this._createButton(
options.locationContent,
options.locationTitle,
"map-location",
container,
this._location
)
this._updateDisabled()
return container
},
disable: function () {
this._disabled = true
this._updateDisabled()
return this
},
enable: function () {
this._disabled = false
this._updateDisabled()
return this
},
_location: function () {
if (this._disabled == true) {
return
}
this.disable()
const success = pos => {
this._map.closePopup()
if (typeof this.options.onLocationSuccess === "function") {
this.options.onLocationSuccess({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
})
}
}
const error = err => {
if (typeof this.options.onLocationFail === "function") {
this.options.onLocationFail(err)
}
}
this._getPosition()
.then(success)
.catch(error)
.finally(() => {
this.enable()
})
},
_getPosition: function () {
var options = {
enableHighAccuracy: false,
timeout: 5000,
maximumAge: 30000,
}
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options)
})
},
_createButton: createButton,
_updateDisabled: function () {
let disabledClassName = "leaflet-disabled"
L.DomUtil.removeClass(this._locationButton, disabledClassName)
this._locationButton.setAttribute("aria-disabled", "false")
if (this._disabled) {
L.DomUtil.addClass(this._locationButton, disabledClassName)
this._locationButton.setAttribute("aria-disabled", "true")
}
},
})
const initLocationControl = () => {
L.Map.mergeOptions({
location: false,
onLocationFail: null,
onLocationSuccess: null,
})
L.Map.addInitHook(function () {
if (this.options.location) {
this.localControl = new LocationControl()
this.addControl(this.LocationControl)
} else {
this.localControl = null
}
})
}
const initMapControls = () => {
initFullScreenControl()
initLocationControl()
}
export {
initFullScreenControl,
initLocationControl,
initMapControls,
FullScreenControl,
LocationControl,
}

View File

@ -31,6 +31,7 @@ export { default as cardstat } from "./CardStat.svelte"
export { default as spectrumcard } from "./SpectrumCard.svelte"
export { default as tag } from "./Tag.svelte"
export { default as markdownviewer } from "./MarkdownViewer.svelte"
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
export * from "./charts"
export * from "./forms"
export * from "./table"

View File

@ -448,6 +448,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
domhandler@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626"
integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==
dependencies:
domelementtype "^2.2.0"
domhandler@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
@ -455,7 +462,12 @@ domhandler@^4.2.0:
dependencies:
domelementtype "^2.2.0"
domutils@^2.6.0:
domino@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe"
integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==
domutils@^2.5.2, domutils@^2.6.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
@ -489,6 +501,11 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
entities@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -499,6 +516,11 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
estree-walker@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e"
@ -519,6 +541,11 @@ estree-walker@^2.0.1:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=
eventemitter3@^4.0.4:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@ -656,6 +683,14 @@ is-reference@^1.2.1:
dependencies:
"@types/estree" "*"
is-regex@^1.0.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
dependencies:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
is-resolvable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
@ -694,6 +729,13 @@ lilconfig@^2.0.3:
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082"
integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==
linkify-it@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e"
integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==
dependencies:
uc.micro "^1.0.1"
loader-utils@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
@ -730,6 +772,11 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@ -1117,6 +1164,27 @@ promise.series@^0.2.0:
resolved "https://registry.yarnpkg.com/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd"
integrity sha1-LMfr6Vn8OmYZwEq029yeRS2GS70=
quill-delta@^3.6.2:
version "3.6.3"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
integrity sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==
dependencies:
deep-equal "^1.0.1"
extend "^3.0.2"
fast-diff "1.1.2"
quill@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8"
integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==
dependencies:
clone "^2.1.1"
deep-equal "^1.0.1"
eventemitter3 "^2.0.3"
extend "^3.0.2"
parchment "^1.1.4"
quill-delta "^3.6.2"
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -1271,6 +1339,27 @@ shortid@^2.2.15:
dependencies:
nanoid "^2.1.0"
sirv-cli@^0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/sirv-cli/-/sirv-cli-0.4.6.tgz#c28ab20deb3b34637f5a60863dc350f055abca04"
integrity sha512-/Vj85/kBvPL+n9ibgX6FicLE8VjidC1BhlX67PYPBfbBAphzR6i0k0HtU5c2arejfU3uzq8l3SYPCwl1x7z6Ww==
dependencies:
console-clear "^1.1.0"
get-port "^3.2.0"
kleur "^3.0.0"
local-access "^1.0.1"
sade "^1.4.0"
sirv "^0.4.6"
tinydate "^1.0.0"
sirv@^0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/sirv/-/sirv-0.4.6.tgz#185e44eb93d24009dd183b7494285c5180b81f22"
integrity sha512-rYpOXlNbpHiY4nVXxuDf4mXPvKz1reZGap/LkWp9TvcZ84qD/nPBjjH/6GZsgIjVMbOslnY8YYULAyP8jMn1GQ==
dependencies:
"@polka/url" "^0.5.0"
mime "^2.3.1"
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"