wip: New assets system

This commit is contained in:
Dan Harrin
2022-10-14 11:33:23 +01:00
parent 44d28124a8
commit 25ae6a6efb
68 changed files with 6195 additions and 38596 deletions
+39 -2
View File
@@ -1,7 +1,11 @@
const esbuild = require('esbuild')
const shouldWatch = process.argv.includes('--watch')
const packages = ['forms', 'notifications']
const packages = [
'forms',
'notifications',
'support',
]
packages.forEach((package) => {
esbuild
@@ -12,11 +16,44 @@ packages.forEach((package) => {
: `'development'`,
},
entryPoints: [`packages/${package}/resources/js/index.js`],
outfile: `packages/${package}/dist/module.esm.js`,
outfile: `packages/${package}/dist/index.js`,
bundle: true,
platform: 'neutral',
mainFields: ['module', 'main'],
watch: shouldWatch,
minify: true,
})
.catch(() => process.exit(1))
})
const formComponents = [
'color-picker',
'date-time-picker',
'file-upload',
'key-value',
'markdown-editor',
'rich-editor',
'select',
'tags-input',
'text-input',
'textarea',
]
formComponents.forEach((component) => {
esbuild
.build({
define: {
'process.env.NODE_ENV': shouldWatch
? `'production'`
: `'development'`,
},
entryPoints: [`packages/forms/resources/js/components/${component}.js`],
outfile: `packages/forms/dist/components/${component}.js`,
bundle: true,
platform: 'neutral',
mainFields: ['module', 'main'],
watch: shouldWatch,
minify: true,
})
.catch(() => process.exit(1))
})
+13
View File
@@ -18,6 +18,7 @@
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.0",
"alpinejs": "^3.9.0",
"async-alpine": "^0.5.3",
"autoprefixer": "^10.4.0",
"chart.js": "^3.6.0",
"choices.js": "^10.1.0",
@@ -2739,6 +2740,12 @@
"inherits": "2.0.1"
}
},
"node_modules/async-alpine": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/async-alpine/-/async-alpine-0.5.3.tgz",
"integrity": "sha512-xg6HzfYzzmD68sY5okWMmK5yukdQ2kMvEONtqLhA/PgQ9YbO2kwRrSsFpJQixo+FDx23vZWBz+BYWob6yKNOtw==",
"dev": true
},
"node_modules/autoprefixer": {
"version": "10.4.8",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
@@ -12572,6 +12579,12 @@
}
}
},
"async-alpine": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/async-alpine/-/async-alpine-0.5.3.tgz",
"integrity": "sha512-xg6HzfYzzmD68sY5okWMmK5yukdQ2kMvEONtqLhA/PgQ9YbO2kwRrSsFpJQixo+FDx23vZWBz+BYWob6yKNOtw==",
"dev": true
},
"autoprefixer": {
"version": "10.4.8",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
+1
View File
@@ -22,6 +22,7 @@
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.0",
"alpinejs": "^3.9.0",
"async-alpine": "^0.5.3",
"autoprefixer": "^10.4.0",
"chart.js": "^3.6.0",
"choices.js": "^10.1.0",
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -59
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,5 +1,5 @@
{
"/app.js": "/app.js?id=607fd92a23f9f410572ed7ef87051a20",
"/app.js": "/app.js?id=e74a8988f18c007aef6877ee19dfa3f6",
"/echo.js": "/echo.js?id=77a99c0e4c69337c7910facf3da252aa",
"/app.css": "/app.css?id=11de9cc2e15849297fd09638257ddb61"
"/app.css": "/app.css?id=ec35dc7910ed78a45910e992c29d0a9f"
}
+1 -1
View File
@@ -1,4 +1,4 @@
@import '../../../forms/dist/module.esm.css';
@import '../../../forms/dist/index.css';
@import 'tippy.js/dist/tippy.css';
@import 'tippy.js/themes/light.css';
-4
View File
@@ -2,17 +2,13 @@ import Alpine from 'alpinejs'
import Chart from 'chart.js/auto'
import Collapse from '@alpinejs/collapse'
import Focus from '@alpinejs/focus'
import FormsAlpinePlugin from '../../../forms/dist/module.esm'
import Mousetrap from '@danharrin/alpine-mousetrap'
import NotificationsAlpinePlugin from '../../../notifications/dist/module.esm'
import Persist from '@alpinejs/persist'
import Tooltip from '@ryangjchandler/alpine-tooltip'
Alpine.plugin(Collapse)
Alpine.plugin(Focus)
Alpine.plugin(FormsAlpinePlugin)
Alpine.plugin(Mousetrap)
Alpine.plugin(NotificationsAlpinePlugin)
Alpine.plugin(Persist)
Alpine.plugin(Tooltip)
@@ -45,17 +45,7 @@
<link href="{{ $fontsUrl }}" rel="stylesheet" />
@endif
@foreach (\Filament\Facades\Filament::getStyles() as $name => $path)
@if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
<link rel="stylesheet" href="{{ $path }}" />
@elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
{!! $path !!}
@else
<link rel="stylesheet" href="{{ route('filament.asset', [
'file' => "{$name}.css",
]) }}" />
@endif
@endforeach
@filamentStyles
{{ \Filament\Facades\Filament::getThemeLink() }}
@@ -90,19 +80,9 @@
window.filamentData = @json(\Filament\Facades\Filament::getScriptData());
</script>
@foreach (\Filament\Facades\Filament::getBeforeCoreScripts() as $name => $path)
@if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
<script defer src="{{ $path }}"></script>
@elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
{!! $path !!}
@else
<script defer src="{{ route('filament.asset', [
'file' => "{$name}.js",
]) }}"></script>
@endif
@endforeach
@filamentScripts
@stack('beforeCoreScripts')
@stack('scripts')
<script defer src="{{ route('filament.asset', [
'id' => Filament\get_asset_id('app.js'),
@@ -124,20 +104,6 @@
</script>
@endif
@foreach (\Filament\Facades\Filament::getScripts() as $name => $path)
@if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
<script defer src="{{ $path }}"></script>
@elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
{!! $path !!}
@else
<script defer src="{{ route('filament.asset', [
'file' => "{$name}.js",
]) }}"></script>
@endif
@endforeach
@stack('scripts')
{{ \Filament\Facades\Filament::renderHook('scripts.end') }}
{{ \Filament\Facades\Filament::renderHook('body.end') }}
-35
View File
@@ -35,14 +35,8 @@ class FilamentManager
protected array $resources = [];
protected array $beforeCoreScripts = [];
protected array $scripts = [];
protected array $scriptData = [];
protected array $styles = [];
protected array $meta = [];
protected string | Htmlable | null $theme = null;
@@ -120,25 +114,11 @@ class FilamentManager
$this->resources = array_merge($this->resources, $resources);
}
public function registerScripts(array $scripts, bool $shouldBeLoadedBeforeCoreScripts = false): void
{
if ($shouldBeLoadedBeforeCoreScripts) {
$this->beforeCoreScripts = array_merge($this->beforeCoreScripts, $scripts);
} else {
$this->scripts = array_merge($this->scripts, $scripts);
}
}
public function registerScriptData(array $data): void
{
$this->scriptData = array_merge($this->scriptData, $data);
}
public function registerStyles(array $styles): void
{
$this->styles = array_merge($this->styles, $styles);
}
public function registerTheme(string | Htmlable | null $theme): void
{
$this->theme = $theme;
@@ -295,26 +275,11 @@ class FilamentManager
return null;
}
public function getScripts(): array
{
return $this->scripts;
}
public function getBeforeCoreScripts(): array
{
return $this->beforeCoreScripts;
}
public function getScriptData(): array
{
return $this->scriptData;
}
public function getStyles(): array
{
return $this->styles;
}
public function getThemeLink(): Htmlable
{
if (Str::of($this->theme)->contains('<link')) {
+3 -3
View File
@@ -67,9 +67,9 @@ abstract class PluginServiceProvider extends PackageServiceProvider
Facades\Filament::serving(function () {
Facades\Filament::registerUserMenuItems($this->getUserMenuItems());
Facades\Filament::registerScripts($this->getBeforeCoreScripts(), true);
Facades\Filament::registerScripts($this->getScripts());
Facades\Filament::registerStyles($this->getStyles());
// Facades\Filament::registerScripts($this->getBeforeCoreScripts(), true);
// Facades\Filament::registerScripts($this->getScripts());
// Facades\Filament::registerStyles($this->getStyles());
Facades\Filament::registerScriptData($this->getScriptData());
});
});
@@ -127,7 +127,8 @@ class EditRecord extends Page implements HasFormActions
return Notification::make()
->success()
->title($this->getSavedNotificationTitle());
->title($this->getSavedNotificationTitle())
->persistent();
}
protected function getSavedNotificationTitle(): ?string
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function o({state:i}){return{state:i,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0&&this.addRow(),this.shouldUpdateRows=!0,this.$watch("state",()=>{if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState(),this.shouldUpdateRows=!0},reorderRows:function(t){let e=Alpine.raw(this.rows),s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.rows=e,this.updateState()},updateRows:function(){let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{o as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function t({state:e}){return{newTag:"",state:e,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(n){this.state=this.state.filter(i=>i!==n)}}}export{t as default};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function t(){return{init:function(){this.$nextTick(()=>{this.render()})},render:function(){this.$el.scrollHeight>0&&(this.$el.style.height="150px",this.$el.style.height=this.$el.scrollHeight+2+"px")}}}export{t as default};
+1
View File
File diff suppressed because one or more lines are too long
View File
-1820
View File
File diff suppressed because it is too large Load Diff
-35353
View File
File diff suppressed because it is too large Load Diff
@@ -3,55 +3,48 @@ import 'vanilla-colorful/hsl-string-color-picker'
import 'vanilla-colorful/rgb-string-color-picker'
import 'vanilla-colorful/rgba-string-color-picker'
import '../../css/components/color-picker.css'
export default function colorPickerFormComponent({ isAutofocused, isDisabled, state }) {
return {
state,
export default (Alpine) => {
Alpine.data(
'colorPickerFormComponent',
({ isAutofocused, isDisabled, state }) => {
return {
state,
init: function () {
if (!(this.state === null || this.state === '')) {
this.setState(this.state)
}
if (isAutofocused) {
this.togglePanelVisibility(this.$refs.input)
}
this.$refs.input.addEventListener('change', (event) => {
this.setState(event.target.value)
})
this.$refs.panel.addEventListener(
'color-changed',
(event) => {
this.setState(event.detail.value)
},
)
},
togglePanelVisibility: function () {
if (isDisabled) {
return
}
this.$refs.panel.toggle(this.$refs.input)
},
setState: function (value) {
this.state = value
this.$refs.input.value = value
this.$refs.panel.color = value
},
isOpen: function () {
return this.$refs.panel.style.display === 'block'
},
init: function () {
if (!(this.state === null || this.state === '')) {
this.setState(this.state)
}
if (isAutofocused) {
this.togglePanelVisibility(this.$refs.input)
}
this.$refs.input.addEventListener('change', (event) => {
this.setState(event.target.value)
})
this.$refs.panel.addEventListener(
'color-changed',
(event) => {
this.setState(event.detail.value)
},
)
},
)
togglePanelVisibility: function () {
if (isDisabled) {
return
}
this.$refs.panel.toggle(this.$refs.input)
},
setState: function (value) {
this.state = value
this.$refs.input.value = value
this.$refs.panel.color = value
},
isOpen: function () {
return this.$refs.panel.style.display === 'block'
},
}
}
@@ -11,479 +11,474 @@ dayjs.extend(utc)
window.dayjs = dayjs
export default (Alpine) => {
Alpine.data(
'dateTimePickerFormComponent',
({
displayFormat,
firstDayOfWeek,
isAutofocused,
locale,
shouldCloseOnDateSelection,
state,
}) => {
const timezone = dayjs.tz.guess()
export default function dateTimePickerFormComponent({
displayFormat,
firstDayOfWeek,
isAutofocused,
locale,
shouldCloseOnDateSelection,
state,
}) {
const timezone = dayjs.tz.guess()
return {
daysInFocusedMonth: [],
return {
daysInFocusedMonth: [],
displayText: '',
displayText: '',
emptyDaysInFocusedMonth: [],
emptyDaysInFocusedMonth: [],
focusedDate: null,
focusedDate: null,
focusedMonth: null,
focusedMonth: null,
focusedYear: null,
focusedYear: null,
hour: null,
hour: null,
isClearingState: false,
isClearingState: false,
minute: null,
minute: null,
second: null,
second: null,
state,
state,
dayLabels: [],
dayLabels: [],
months: [],
months: [],
init: function () {
dayjs.locale(locales[locale] ?? locales['en'])
init: function () {
dayjs.locale(locales[locale] ?? locales['en'])
this.focusedDate = dayjs().tz(timezone)
this.focusedDate = dayjs().tz(timezone)
let date =
this.getSelectedDate() ??
dayjs().tz(timezone).hour(0).minute(0).second(0)
let date =
this.getSelectedDate() ??
dayjs().tz(timezone).hour(0).minute(0).second(0)
if (
this.getMaxDate() !== null &&
date.isAfter(this.getMaxDate())
) {
date = null
} else if (
this.getMinDate() !== null &&
date.isBefore(this.getMinDate())
) {
date = null
}
if (
this.getMaxDate() !== null &&
date.isAfter(this.getMaxDate())
) {
date = null
} else if (
this.getMinDate() !== null &&
date.isBefore(this.getMinDate())
) {
date = null
}
this.hour = date?.hour() ?? 0
this.minute = date?.minute() ?? 0
this.second = date?.second() ?? 0
this.hour = date?.hour() ?? 0
this.minute = date?.minute() ?? 0
this.second = date?.second() ?? 0
this.setDisplayText()
this.setMonths()
this.setDayLabels()
this.setDisplayText()
this.setMonths()
this.setDayLabels()
if (isAutofocused) {
this.$nextTick(() =>
this.togglePanelVisibility(this.$refs.button),
)
}
if (isAutofocused) {
this.$nextTick(() =>
this.togglePanelVisibility(this.$refs.button),
)
}
this.$watch('focusedMonth', () => {
this.focusedMonth = +this.focusedMonth
this.$watch('focusedMonth', () => {
this.focusedMonth = +this.focusedMonth
if (this.focusedDate.month() === this.focusedMonth) {
return
}
if (this.focusedDate.month() === this.focusedMonth) {
return
}
this.focusedDate = this.focusedDate.month(
this.focusedMonth,
)
})
this.focusedDate = this.focusedDate.month(
this.focusedMonth,
)
})
this.$watch('focusedYear', () => {
if (this.focusedYear?.length > 4) {
this.focusedYear = this.focusedYear.substring(0, 4)
}
this.$watch('focusedYear', () => {
if (this.focusedYear?.length > 4) {
this.focusedYear = this.focusedYear.substring(0, 4)
}
if (
!this.focusedYear ||
this.focusedYear?.length !== 4
) {
return
}
if (
!this.focusedYear ||
this.focusedYear?.length !== 4
) {
return
}
let year = +this.focusedYear
let year = +this.focusedYear
if (!Number.isInteger(year)) {
year = dayjs().tz(timezone).year()
if (!Number.isInteger(year)) {
year = dayjs().tz(timezone).year()
this.focusedYear = year
}
this.focusedYear = year
}
if (this.focusedDate.year() === year) {
return
}
if (this.focusedDate.year() === year) {
return
}
this.focusedDate = this.focusedDate.year(year)
})
this.focusedDate = this.focusedDate.year(year)
})
this.$watch('focusedDate', () => {
let month = this.focusedDate.month()
let year = this.focusedDate.year()
this.$watch('focusedDate', () => {
let month = this.focusedDate.month()
let year = this.focusedDate.year()
if (this.focusedMonth !== month) {
this.focusedMonth = month
}
if (this.focusedMonth !== month) {
this.focusedMonth = month
}
if (this.focusedYear !== year) {
this.focusedYear = year
}
if (this.focusedYear !== year) {
this.focusedYear = year
}
this.setupDaysGrid()
})
this.setupDaysGrid()
})
this.$watch('hour', () => {
let hour = +this.hour
if (!Number.isInteger(hour)) {
this.hour = 0
} else if (hour > 23) {
this.hour = 0
} else if (hour < 0) {
this.hour = 23
} else {
this.hour = hour
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.hour(this.hour ?? 0))
})
this.$watch('minute', () => {
let minute = +this.minute
if (!Number.isInteger(minute)) {
this.minute = 0
} else if (minute > 59) {
this.minute = 0
} else if (minute < 0) {
this.minute = 59
} else {
this.minute = minute
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.minute(this.minute ?? 0))
})
this.$watch('second', () => {
let second = +this.second
if (!Number.isInteger(second)) {
this.second = 0
} else if (second > 59) {
this.second = 0
} else if (second < 0) {
this.second = 59
} else {
this.second = second
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.second(this.second ?? 0))
})
this.$watch('state', () => {
if (this.state === undefined) {
return
}
let date = this.getSelectedDate()
if (date === null) {
this.clearState()
return
}
if (
this.getMaxDate() !== null &&
date?.isAfter(this.getMaxDate())
) {
date = null
}
if (
this.getMinDate() !== null &&
date?.isBefore(this.getMinDate())
) {
date = null
}
const newHour = date?.hour() ?? 0
if (this.hour !== newHour) {
this.hour = newHour
}
const newMinute = date?.minute() ?? 0
if (this.minute !== newMinute) {
this.minute = newMinute
}
const newSecond = date?.second() ?? 0
if (this.second !== newSecond) {
this.second = newSecond
}
this.setDisplayText()
})
},
clearState: function () {
this.isClearingState = true
this.setState(null)
this.$watch('hour', () => {
let hour = +this.hour
if (!Number.isInteger(hour)) {
this.hour = 0
} else if (hour > 23) {
this.hour = 0
} else if (hour < 0) {
this.hour = 23
} else {
this.hour = hour
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.hour(this.hour ?? 0))
})
this.$watch('minute', () => {
let minute = +this.minute
if (!Number.isInteger(minute)) {
this.minute = 0
} else if (minute > 59) {
this.minute = 0
} else if (minute < 0) {
this.minute = 59
} else {
this.minute = minute
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.minute(this.minute ?? 0))
})
this.$watch('second', () => {
let second = +this.second
if (!Number.isInteger(second)) {
this.second = 0
} else if (second > 59) {
this.second = 0
} else if (second < 0) {
this.second = 59
} else {
this.second = second
}
this.$nextTick(() => (this.isClearingState = false))
},
if (this.isClearingState) {
return
}
dateIsDisabled: function (date) {
if (
JSON.parse(this.$refs.disabledDates?.value ?? []).some(
(disabledDate) => {
disabledDate = dayjs(disabledDate)
let date = this.getSelectedDate() ?? this.focusedDate
if (!disabledDate.isValid()) {
return false
}
this.setState(date.second(this.second ?? 0))
})
return disabledDate.isSame(date, 'day')
},
)
) {
return true
}
this.$watch('state', () => {
if (this.state === undefined) {
return
}
if (this.getMaxDate() && date.isAfter(this.getMaxDate())) {
return true
}
if (this.getMinDate() && date.isBefore(this.getMinDate())) {
return true
}
let date = this.getSelectedDate()
return false
},
if (date === null) {
this.clearState()
dayIsDisabled: function (day) {
this.focusedDate ??= dayjs().tz(timezone)
return
}
return this.dateIsDisabled(this.focusedDate.date(day))
},
if (
this.getMaxDate() !== null &&
date?.isAfter(this.getMaxDate())
) {
date = null
}
if (
this.getMinDate() !== null &&
date?.isBefore(this.getMinDate())
) {
date = null
}
dayIsSelected: function (day) {
let selectedDate = this.getSelectedDate()
const newHour = date?.hour() ?? 0
if (this.hour !== newHour) {
this.hour = newHour
}
if (selectedDate === null) {
return false
}
const newMinute = date?.minute() ?? 0
if (this.minute !== newMinute) {
this.minute = newMinute
}
this.focusedDate ??= dayjs().tz(timezone)
const newSecond = date?.second() ?? 0
if (this.second !== newSecond) {
this.second = newSecond
}
return (
selectedDate.date() === day &&
selectedDate.month() === this.focusedDate.month() &&
selectedDate.year() === this.focusedDate.year()
)
},
this.setDisplayText()
})
},
dayIsToday: function (day) {
let date = dayjs().tz(timezone)
this.focusedDate ??= date
clearState: function () {
this.isClearingState = true
return (
date.date() === day &&
date.month() === this.focusedDate.month() &&
date.year() === this.focusedDate.year()
)
},
this.setState(null)
focusPreviousDay: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.hour = 0
this.minute = 0
this.second = 0
this.focusedDate = this.focusedDate.subtract(1, 'day')
},
this.$nextTick(() => (this.isClearingState = false))
},
focusPreviousWeek: function () {
this.focusedDate ??= dayjs().tz(timezone)
dateIsDisabled: function (date) {
if (
JSON.parse(this.$refs.disabledDates?.value ?? []).some(
(disabledDate) => {
disabledDate = dayjs(disabledDate)
this.focusedDate = this.focusedDate.subtract(1, 'week')
},
if (!disabledDate.isValid()) {
return false
}
focusNextDay: function () {
this.focusedDate ??= dayjs().tz(timezone)
return disabledDate.isSame(date, 'day')
},
)
) {
return true
}
this.focusedDate = this.focusedDate.add(1, 'day')
},
if (this.getMaxDate() && date.isAfter(this.getMaxDate())) {
return true
}
if (this.getMinDate() && date.isBefore(this.getMinDate())) {
return true
}
focusNextWeek: function () {
this.focusedDate ??= dayjs().tz(timezone)
return false
},
this.focusedDate = this.focusedDate.add(1, 'week')
},
dayIsDisabled: function (day) {
this.focusedDate ??= dayjs().tz(timezone)
getDayLabels: function () {
const labels = dayjs.weekdaysShort()
return this.dateIsDisabled(this.focusedDate.date(day))
},
if (firstDayOfWeek === 0) {
return labels
}
dayIsSelected: function (day) {
let selectedDate = this.getSelectedDate()
return [
...labels.slice(firstDayOfWeek),
...labels.slice(0, firstDayOfWeek),
]
},
if (selectedDate === null) {
return false
}
getMaxDate: function () {
let date = dayjs(this.$refs.maxDate?.value)
this.focusedDate ??= dayjs().tz(timezone)
return date.isValid() ? date : null
},
return (
selectedDate.date() === day &&
selectedDate.month() === this.focusedDate.month() &&
selectedDate.year() === this.focusedDate.year()
)
},
getMinDate: function () {
let date = dayjs(this.$refs.minDate?.value)
dayIsToday: function (day) {
let date = dayjs().tz(timezone)
this.focusedDate ??= date
return date.isValid() ? date : null
},
return (
date.date() === day &&
date.month() === this.focusedDate.month() &&
date.year() === this.focusedDate.year()
)
},
getSelectedDate: function () {
if (this.state === undefined) {
return null
}
focusPreviousDay: function () {
this.focusedDate ??= dayjs().tz(timezone)
if (this.state === null) {
return null
}
this.focusedDate = this.focusedDate.subtract(1, 'day')
},
let date = dayjs(this.state)
focusPreviousWeek: function () {
this.focusedDate ??= dayjs().tz(timezone)
if (!date.isValid()) {
return null
}
this.focusedDate = this.focusedDate.subtract(1, 'week')
},
return date
},
focusNextDay: function () {
this.focusedDate ??= dayjs().tz(timezone)
togglePanelVisibility: function () {
if (!this.isOpen()) {
this.focusedDate =
this.getSelectedDate() ??
this.getMinDate() ??
dayjs().tz(timezone)
this.focusedDate = this.focusedDate.add(1, 'day')
},
this.setupDaysGrid()
}
focusNextWeek: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.$refs.panel.toggle(this.$refs.button)
},
this.focusedDate = this.focusedDate.add(1, 'week')
},
selectDate: function (day = null) {
if (day) {
this.setFocusedDay(day)
}
getDayLabels: function () {
const labels = dayjs.weekdaysShort()
this.focusedDate ??= dayjs().tz(timezone)
if (firstDayOfWeek === 0) {
return labels
}
this.setState(this.focusedDate)
return [
...labels.slice(firstDayOfWeek),
...labels.slice(0, firstDayOfWeek),
]
},
if (shouldCloseOnDateSelection) {
this.togglePanelVisibility()
}
},
getMaxDate: function () {
let date = dayjs(this.$refs.maxDate?.value)
setDisplayText: function () {
this.displayText = this.getSelectedDate()
? this.getSelectedDate().format(displayFormat)
: ''
},
return date.isValid() ? date : null
},
setMonths: function () {
this.months = dayjs.months()
},
getMinDate: function () {
let date = dayjs(this.$refs.minDate?.value)
setDayLabels: function () {
this.dayLabels = this.getDayLabels()
},
return date.isValid() ? date : null
},
setupDaysGrid: function () {
this.focusedDate ??= dayjs().tz(timezone)
getSelectedDate: function () {
if (this.state === undefined) {
return null
}
this.emptyDaysInFocusedMonth = Array.from(
{
length: this.focusedDate
.date(8 - firstDayOfWeek)
.day(),
},
(_, i) => i + 1,
)
if (this.state === null) {
return null
}
this.daysInFocusedMonth = Array.from(
{
length: this.focusedDate.daysInMonth(),
},
(_, i) => i + 1,
)
},
let date = dayjs(this.state)
setFocusedDay: function (day) {
this.focusedDate = (
this.focusedDate ?? dayjs().tz(timezone)
).date(day)
},
if (!date.isValid()) {
return null
}
setState: function (date) {
if (date === null) {
this.state = null
this.setDisplayText()
return date
},
return
}
togglePanelVisibility: function () {
if (!this.isOpen()) {
this.focusedDate =
this.getSelectedDate() ??
this.getMinDate() ??
dayjs().tz(timezone)
if (this.dateIsDisabled(date)) {
return
}
this.setupDaysGrid()
}
this.state = date
.hour(this.hour ?? 0)
.minute(this.minute ?? 0)
.second(this.second ?? 0)
.format('YYYY-MM-DD HH:mm:ss')
this.$refs.panel.toggle(this.$refs.button)
},
this.setDisplayText()
},
selectDate: function (day = null) {
if (day) {
this.setFocusedDay(day)
}
isOpen: function () {
return this.$refs.panel?.style.display === 'block'
},
this.focusedDate ??= dayjs().tz(timezone)
this.setState(this.focusedDate)
if (shouldCloseOnDateSelection) {
this.togglePanelVisibility()
}
},
)
setDisplayText: function () {
this.displayText = this.getSelectedDate()
? this.getSelectedDate().format(displayFormat)
: ''
},
setMonths: function () {
this.months = dayjs.months()
},
setDayLabels: function () {
this.dayLabels = this.getDayLabels()
},
setupDaysGrid: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.emptyDaysInFocusedMonth = Array.from(
{
length: this.focusedDate
.date(8 - firstDayOfWeek)
.day(),
},
(_, i) => i + 1,
)
this.daysInFocusedMonth = Array.from(
{
length: this.focusedDate.daysInMonth(),
},
(_, i) => i + 1,
)
},
setFocusedDay: function (day) {
this.focusedDate = (
this.focusedDate ?? dayjs().tz(timezone)
).date(day)
},
setState: function (date) {
if (date === null) {
this.state = null
this.setDisplayText()
return
}
if (this.dateIsDisabled(date)) {
return
}
this.state = date
.hour(this.hour ?? 0)
.minute(this.minute ?? 0)
.second(this.second ?? 0)
.format('YYYY-MM-DD HH:mm:ss')
this.setDisplayText()
},
isOpen: function () {
return this.$refs.panel?.style.display === 'block'
},
}
}
const locales = {
@@ -8,11 +8,6 @@ import FilePondPluginImageResize from 'filepond-plugin-image-resize'
import FilePondPluginImageTransform from 'filepond-plugin-image-transform'
import FilePondPluginMediaPreview from 'filepond-plugin-media-preview'
import 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css'
import 'filepond-plugin-media-preview/dist/filepond-plugin-media-preview.css'
import '../../css/components/file-upload.css'
FilePond.registerPlugin(FilePondPluginFileValidateSize)
FilePond.registerPlugin(FilePondPluginFileValidateType)
FilePond.registerPlugin(FilePondPluginImageCrop)
@@ -24,354 +19,349 @@ FilePond.registerPlugin(FilePondPluginMediaPreview)
window.FilePond = FilePond
export default (Alpine) => {
Alpine.data(
'fileUploadFormComponent',
({
acceptedFileTypes,
canDownload,
canOpen,
canPreview,
canReorder,
deleteUploadedFileUsing,
getUploadedFileUrlsUsing,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeMode,
imageResizeTargetHeight,
imageResizeTargetWidth,
isAvatar,
loadingIndicatorPosition,
locale,
panelAspectRatio,
panelLayout,
placeholder,
maxSize,
minSize,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
reorderUploadedFilesUsing,
shouldAppendFiles,
shouldTransformImage,
state,
uploadButtonPosition,
uploadProgressIndicatorPosition,
uploadUsing,
}) => {
return {
fileKeyIndex: {},
export default function fileUploadFormComponent({
acceptedFileTypes,
canDownload,
canOpen,
canPreview,
canReorder,
deleteUploadedFileUsing,
getUploadedFileUrlsUsing,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeMode,
imageResizeTargetHeight,
imageResizeTargetWidth,
isAvatar,
loadingIndicatorPosition,
locale,
panelAspectRatio,
panelLayout,
placeholder,
maxSize,
minSize,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
reorderUploadedFilesUsing,
shouldAppendFiles,
shouldTransformImage,
state,
uploadButtonPosition,
uploadProgressIndicatorPosition,
uploadUsing,
}) {
return {
fileKeyIndex: {},
pond: null,
pond: null,
shouldUpdateState: true,
shouldUpdateState: true,
state,
state,
lastState: null,
lastState: null,
uploadedFileUrlIndex: {},
uploadedFileUrlIndex: {},
init: async function () {
FilePond.setOptions(locales[locale] ?? locales['en'])
init: async function () {
FilePond.setOptions(locales[locale] ?? locales['en'])
this.pond = FilePond.create(this.$refs.input, {
acceptedFileTypes,
allowPaste: false,
allowReorder: canReorder,
allowImagePreview: canPreview,
allowVideoPreview: canPreview,
allowAudioPreview: canPreview,
allowImageTransform: shouldTransformImage,
credits: false,
files: await this.getFiles(),
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeMode,
itemInsertLocation: shouldAppendFiles
? 'after'
: 'before',
...(placeholder && { labelIdle: placeholder }),
maxFileSize: maxSize,
minFileSize: minSize,
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition:
removeUploadedFileButtonPosition,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
styleProgressIndicatorPosition:
uploadProgressIndicatorPosition,
server: {
load: async (source, load) => {
let response = await fetch(source)
let blob = await response.blob()
this.pond = FilePond.create(this.$refs.input, {
acceptedFileTypes,
allowPaste: false,
allowReorder: canReorder,
allowImagePreview: canPreview,
allowVideoPreview: canPreview,
allowAudioPreview: canPreview,
allowImageTransform: shouldTransformImage,
credits: false,
files: await this.getFiles(),
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeMode,
itemInsertLocation: shouldAppendFiles
? 'after'
: 'before',
...(placeholder && { labelIdle: placeholder }),
maxFileSize: maxSize,
minFileSize: minSize,
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition:
removeUploadedFileButtonPosition,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
styleProgressIndicatorPosition:
uploadProgressIndicatorPosition,
server: {
load: async (source, load) => {
let response = await fetch(source)
let blob = await response.blob()
load(blob)
},
process: (
fieldName,
file,
metadata,
load,
error,
progress,
) => {
this.shouldUpdateState = false
load(blob)
},
process: (
fieldName,
file,
metadata,
load,
error,
progress,
) => {
this.shouldUpdateState = false
let fileKey = (
[1e7] +
-1e3 +
-4e3 +
-8e3 +
-1e11
).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(
new Uint8Array(1),
)[0] &
(15 >> (c / 4)))
).toString(16),
)
uploadUsing(
fileKey,
file,
(fileKey) => {
this.shouldUpdateState = true
load(fileKey)
},
error,
progress,
)
},
remove: async (source, load) => {
let fileKey =
this.uploadedFileUrlIndex[source] ?? null
if (!fileKey) {
return
}
await deleteUploadedFileUsing(fileKey)
load()
},
revert: async (uniqueFileId, load) => {
await removeUploadedFileUsing(uniqueFileId)
load()
},
},
})
this.$watch('state', async () => {
if (!this.shouldUpdateState) {
return
}
// We don't want to overwrite the files that are already in the input, if they haven't been saved yet.
if (
Object.values(this.state).filter((file) =>
file.startsWith('livewire-file:'),
).length
) {
return
}
// Don't do anything if the state hasn't changed
if (JSON.stringify(this.state) === this.lastState) {
return
}
this.lastState = JSON.stringify(this.state)
this.pond.files = await this.getFiles()
})
this.pond.on('reorderfiles', async (files) => {
const orderedFileKeys = files
.map((file) =>
file.source instanceof File
? file.serverId
: this.uploadedFileUrlIndex[file.source] ??
null,
) // file.serverId is null for a file that is not yet uploaded
.filter((fileKey) => fileKey)
await reorderUploadedFilesUsing(
shouldAppendFiles
? orderedFileKeys
: orderedFileKeys.reverse(),
let fileKey = (
[1e7] +
-1e3 +
-4e3 +
-8e3 +
-1e11
).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(
new Uint8Array(1),
)[0] &
(15 >> (c / 4)))
).toString(16),
)
})
this.pond.on('initfile', async (fileItem) => {
if (!canDownload) {
return
}
uploadUsing(
fileKey,
file,
(fileKey) => {
this.shouldUpdateState = true
if (isAvatar) {
return
}
this.insertDownloadLink(fileItem)
})
this.pond.on('initfile', async (fileItem) => {
if (!canOpen) {
return
}
if (isAvatar) {
return
}
this.insertOpenLink(fileItem)
})
this.pond.on('processfilestart', async () => {
this.dispatchFormEvent('file-upload-started')
})
this.pond.on('processfileprogress', async () => {
this.dispatchFormEvent('file-upload-started')
})
this.pond.on('processfile', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfiles', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfileabort', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfilerevert', async () => {
this.dispatchFormEvent('file-upload-finished')
})
},
dispatchFormEvent: function (name) {
this.$el.closest('form')?.dispatchEvent(
new CustomEvent(name, {
composed: true,
cancelable: true,
}),
)
},
getUploadedFileUrls: async function () {
const uploadedFileUrls = await getUploadedFileUrlsUsing()
this.fileKeyIndex = uploadedFileUrls ?? {}
this.uploadedFileUrlIndex = Object.entries(
this.fileKeyIndex,
)
.filter((value) => value)
.reduce((obj, [key, value]) => {
obj[value] = key
return obj
}, {})
},
getFiles: async function () {
await this.getUploadedFileUrls()
let files = []
for (const uploadedFileUrl of Object.values(
this.fileKeyIndex,
)) {
if (!uploadedFileUrl) {
continue
}
files.push({
source: uploadedFileUrl,
options: {
type: 'local',
load(fileKey)
},
})
}
error,
progress,
)
},
remove: async (source, load) => {
let fileKey =
this.uploadedFileUrlIndex[source] ?? null
return shouldAppendFiles ? files : files.reverse()
if (!fileKey) {
return
}
await deleteUploadedFileUsing(fileKey)
load()
},
revert: async (uniqueFileId, load) => {
await removeUploadedFileUsing(uniqueFileId)
load()
},
},
})
insertDownloadLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
this.$watch('state', async () => {
if (!this.shouldUpdateState) {
return
}
const anchor = this.getDownloadLink(file)
// We don't want to overwrite the files that are already in the input, if they haven't been saved yet.
if (
Object.values(this.state).filter((file) =>
file.startsWith('livewire-file:'),
).length
) {
return
}
if (!anchor) {
return
}
// Don't do anything if the state hasn't changed
if (JSON.stringify(this.state) === this.lastState) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
this.lastState = JSON.stringify(this.state)
insertOpenLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
this.pond.files = await this.getFiles()
})
const anchor = this.getOpenLink(file)
this.pond.on('reorderfiles', async (files) => {
const orderedFileKeys = files
.map((file) =>
file.source instanceof File
? file.serverId
: this.uploadedFileUrlIndex[file.source] ??
null,
) // file.serverId is null for a file that is not yet uploaded
.filter((fileKey) => fileKey)
if (!anchor) {
return
}
await reorderUploadedFilesUsing(
shouldAppendFiles
? orderedFileKeys
: orderedFileKeys.reverse(),
)
})
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
this.pond.on('initfile', async (fileItem) => {
if (!canDownload) {
return
}
getDownloadLink: function (file) {
let fileSource = file.source
if (isAvatar) {
return
}
if (!fileSource) {
return
}
this.insertDownloadLink(fileItem)
})
const anchor = document.createElement('a')
anchor.className = 'filepond--download-icon'
anchor.href = fileSource
anchor.download = file.file.name
this.pond.on('initfile', async (fileItem) => {
if (!canOpen) {
return
}
return anchor
},
if (isAvatar) {
return
}
getOpenLink: function (file) {
let fileSource = file.source
this.insertOpenLink(fileItem)
})
if (!fileSource) {
return
}
this.pond.on('processfilestart', async () => {
this.dispatchFormEvent('file-upload-started')
})
const anchor = document.createElement('a')
anchor.className = 'filepond--open-icon'
anchor.href = fileSource
anchor.target = '_blank'
this.pond.on('processfileprogress', async () => {
this.dispatchFormEvent('file-upload-started')
})
return anchor
},
}
this.pond.on('processfile', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfiles', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfileabort', async () => {
this.dispatchFormEvent('file-upload-finished')
})
this.pond.on('processfilerevert', async () => {
this.dispatchFormEvent('file-upload-finished')
})
},
)
dispatchFormEvent: function (name) {
this.$el.closest('form')?.dispatchEvent(
new CustomEvent(name, {
composed: true,
cancelable: true,
}),
)
},
getUploadedFileUrls: async function () {
const uploadedFileUrls = await getUploadedFileUrlsUsing()
this.fileKeyIndex = uploadedFileUrls ?? {}
this.uploadedFileUrlIndex = Object.entries(
this.fileKeyIndex,
)
.filter((value) => value)
.reduce((obj, [key, value]) => {
obj[value] = key
return obj
}, {})
},
getFiles: async function () {
await this.getUploadedFileUrls()
let files = []
for (const uploadedFileUrl of Object.values(
this.fileKeyIndex,
)) {
if (!uploadedFileUrl) {
continue
}
files.push({
source: uploadedFileUrl,
options: {
type: 'local',
},
})
}
return shouldAppendFiles ? files : files.reverse()
},
insertDownloadLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getDownloadLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
insertOpenLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getOpenLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
getDownloadLink: function (file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--download-icon'
anchor.href = fileSource
anchor.download = file.file.name
return anchor
},
getOpenLink: function (file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--open-icon'
anchor.href = fileSource
anchor.target = '_blank'
return anchor
},
}
}
import ar from 'filepond/locale/ar-ar'
@@ -1,5 +1,5 @@
export default (Alpine) => {
Alpine.data('keyValueFormComponent', ({ state }) => ({
export default function keyValueFormComponent ({ state }) {
return {
state,
rows: [],
@@ -88,5 +88,5 @@ export default (Alpine) => {
this.state = state
},
}))
}
}
@@ -4,86 +4,84 @@ import DOMPurify from 'dompurify'
import { marked } from 'marked'
import { highlight } from 'mdhl'
export default (Alpine) => {
Alpine.data('markdownEditorFormComponent', ({ state, tab }) => {
return {
attachment: null,
export default function markdownEditorFormComponent ({ state, tab }) {
return {
attachment: null,
overlay: null,
overlay: null,
preview: '',
preview: '',
state,
state,
tab,
tab,
init: function () {
if (this.state !== null) {
this.$nextTick(() => {
this.render()
})
}
this.$watch('state', () => {
init: function () {
if (this.state !== null) {
this.$nextTick(() => {
this.render()
})
},
render: function () {
if (this.$refs.textarea.scrollHeight > 0) {
this.$refs.overlay.style.height = '150px'
this.$refs.overlay.style.height =
this.$refs.textarea.scrollHeight + 'px'
}
this.state = this.state.replace('\r\n', '\n')
this.overlay = null
this.overlay = highlight(this.state)
this.preview = null
this.preview = DOMPurify.sanitize(marked(this.state))
},
checkForAutoInsertion: function () {
const lines = this.$refs.textarea.value.split('\n')
const currentLine = this.$refs.textarea.value
.substring(0, this.$refs.textarea.value.selectionStart)
.split('\n').length
const previousLine = lines[currentLine - 2]
if (!previousLine.match(/^(\*\s|-\s)|^(\d)+\./)) {
return
}
if (previousLine.match(/^(\*\s)/)) {
if (previousLine.trim().length > 1) {
lines[currentLine - 1] = '* '
} else {
delete lines[currentLine - 2]
}
} else if (previousLine.match(/^(-\s)/)) {
if (previousLine.trim().length > 1) {
lines[currentLine - 1] = '- '
} else {
delete lines[currentLine - 2]
}
} else {
const matches = previousLine.match(/^(\d)+/)
const number = matches[0]
if (previousLine.trim().length > number.length + 2) {
lines[currentLine - 1] = `${parseInt(number) + 1}. `
} else {
delete lines[currentLine - 2]
}
}
this.state = lines.join('\n')
}
this.$watch('state', () => {
this.render()
},
}
})
})
},
render: function () {
if (this.$refs.textarea.scrollHeight > 0) {
this.$refs.overlay.style.height = '150px'
this.$refs.overlay.style.height =
this.$refs.textarea.scrollHeight + 'px'
}
this.state = this.state.replace('\r\n', '\n')
this.overlay = null
this.overlay = highlight(this.state)
this.preview = null
this.preview = DOMPurify.sanitize(marked(this.state))
},
checkForAutoInsertion: function () {
const lines = this.$refs.textarea.value.split('\n')
const currentLine = this.$refs.textarea.value
.substring(0, this.$refs.textarea.value.selectionStart)
.split('\n').length
const previousLine = lines[currentLine - 2]
if (!previousLine.match(/^(\*\s|-\s)|^(\d)+\./)) {
return
}
if (previousLine.match(/^(\*\s)/)) {
if (previousLine.trim().length > 1) {
lines[currentLine - 1] = '* '
} else {
delete lines[currentLine - 2]
}
} else if (previousLine.match(/^(-\s)/)) {
if (previousLine.trim().length > 1) {
lines[currentLine - 1] = '- '
} else {
delete lines[currentLine - 2]
}
} else {
const matches = previousLine.match(/^(\d)+/)
const number = matches[0]
if (previousLine.trim().length > number.length + 2) {
lines[currentLine - 1] = `${parseInt(number) + 1}. `
} else {
delete lines[currentLine - 2]
}
}
this.state = lines.join('\n')
this.render()
},
}
}
@@ -1,8 +1,5 @@
import Trix from 'trix/dist/trix'
import 'trix/dist/trix.css'
import '../../css/components/rich-editor.css'
Trix.config.blockAttributes.default.tagName = 'p'
Trix.config.blockAttributes.default.breakOnReturn = true
@@ -42,22 +39,20 @@ Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
}
}
export default (Alpine) => {
Alpine.data('richEditorFormComponent', ({ state }) => {
return {
state,
export default function richEditorFormComponent({ state }) {
return {
state,
init: function () {
this.$refs.trix?.editor?.loadHTML(this.state)
this.$watch('state', () => {
if (document.activeElement === this.$refs.trix) {
return
}
init: function () {
this.$refs.trix?.editor?.loadHTML(this.state)
this.$watch('state', () => {
if (document.activeElement === this.$refs.trix) {
return
}
this.$refs.trix?.editor?.loadHTML(this.state)
})
},
}
})
})
},
}
}
+227 -234
View File
@@ -1,259 +1,252 @@
import Choices from 'choices.js'
import '../../css/components/select.css'
export default function selectFormComponent({
isHtmlAllowed,
getOptionLabelUsing,
getOptionLabelsUsing,
getOptionsUsing,
getSearchResultsUsing,
isAutofocused,
isMultiple,
hasDynamicOptions,
hasDynamicSearchResults,
loadingMessage,
maxItems,
maxItemsMessage,
noSearchResultsMessage,
options,
optionsLimit,
placeholder,
searchDebounce,
searchingMessage,
searchPrompt,
state,
}) {
return {
isSearching: false,
export default (Alpine) => {
Alpine.data(
'selectFormComponent',
({
isHtmlAllowed,
getOptionLabelUsing,
getOptionLabelsUsing,
getOptionsUsing,
getSearchResultsUsing,
isAutofocused,
isMultiple,
hasDynamicOptions,
hasDynamicSearchResults,
loadingMessage,
maxItems,
maxItemsMessage,
noSearchResultsMessage,
options,
optionsLimit,
placeholder,
searchDebounce,
searchingMessage,
searchPrompt,
state,
}) => {
return {
isSearching: false,
select: null,
select: null,
selectedOptions: [],
selectedOptions: [],
isStateBeingUpdated: false,
isStateBeingUpdated: false,
state,
state,
init: async function () {
this.select = new Choices(this.$refs.input, {
allowHTML: isHtmlAllowed,
duplicateItemsAllowed: false,
itemSelectText: '',
loadingText: loadingMessage,
maxItemCount: maxItems ?? -1,
maxItemText: (maxItemCount) =>
window.pluralize(maxItemsMessage, maxItemCount, {
count: maxItemCount,
}),
noChoicesText: searchPrompt,
noResultsText: noSearchResultsMessage,
placeholderValue: placeholder,
removeItemButton: true,
renderChoiceLimit: optionsLimit,
searchFields: ['label'],
searchResultLimit: optionsLimit,
shouldSort: false,
})
init: async function () {
this.select = new Choices(this.$refs.input, {
allowHTML: isHtmlAllowed,
duplicateItemsAllowed: false,
itemSelectText: '',
loadingText: loadingMessage,
maxItemCount: maxItems ?? -1,
maxItemText: (maxItemCount) =>
window.pluralize(maxItemsMessage, maxItemCount, {
count: maxItemCount,
}),
noChoicesText: searchPrompt,
noResultsText: noSearchResultsMessage,
placeholderValue: placeholder,
removeItemButton: true,
renderChoiceLimit: optionsLimit,
searchFields: ['label'],
searchResultLimit: optionsLimit,
shouldSort: false,
})
await this.refreshChoices({ withInitialOptions: true })
await this.refreshChoices({ withInitialOptions: true })
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(
this.formatState(this.state),
)
}
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(
this.formatState(this.state),
)
}
this.refreshPlaceholder()
this.refreshPlaceholder()
if (isAutofocused) {
this.select.showDropdown()
}
if (isAutofocused) {
this.select.showDropdown()
}
this.$refs.input.addEventListener('change', () => {
this.refreshPlaceholder()
this.$refs.input.addEventListener('change', () => {
this.refreshPlaceholder()
if (this.isStateBeingUpdated) {
return
}
if (this.isStateBeingUpdated) {
this.isStateBeingUpdated = true
this.state = this.select.getValue(true) ?? null
this.$nextTick(() => (this.isStateBeingUpdated = false))
})
if (hasDynamicOptions) {
this.$refs.input.addEventListener(
'showDropdown',
async () => {
this.select.clearChoices()
await this.select.setChoices([
{
label: loadingMessage,
value: '',
disabled: true,
},
])
await this.refreshChoices()
},
)
}
if (hasDynamicSearchResults) {
this.$refs.input.addEventListener(
'search',
async (event) => {
let search = event.detail.value?.trim()
if ([null, undefined, ''].includes(search)) {
return
}
this.isStateBeingUpdated = true
this.state = this.select.getValue(true) ?? null
this.$nextTick(() => (this.isStateBeingUpdated = false))
})
this.isSearching = true
if (hasDynamicOptions) {
this.$refs.input.addEventListener(
'showDropdown',
async () => {
this.select.clearChoices()
await this.select.setChoices([
{
label: loadingMessage,
value: '',
disabled: true,
},
])
await this.refreshChoices()
this.select.clearChoices()
await this.select.setChoices([
{
label: searchingMessage,
value: '',
disabled: true,
},
)
}
])
},
)
if (hasDynamicSearchResults) {
this.$refs.input.addEventListener(
'search',
async (event) => {
let search = event.detail.value?.trim()
if ([null, undefined, ''].includes(search)) {
return
}
this.isSearching = true
this.select.clearChoices()
await this.select.setChoices([
{
label: searchingMessage,
value: '',
disabled: true,
},
])
},
)
this.$refs.input.addEventListener(
'search',
Alpine.debounce(async (event) => {
await this.refreshChoices({
search: event.detail.value?.trim(),
})
this.isSearching = false
}, searchDebounce),
)
}
this.$watch('state', async () => {
this.refreshPlaceholder()
if (this.isStateBeingUpdated) {
return
}
const choices = await this.getChoices({
withInitialOptions: !hasDynamicOptions,
this.$refs.input.addEventListener(
'search',
Alpine.debounce(async (event) => {
await this.refreshChoices({
search: event.detail.value?.trim(),
})
this.select.clearStore()
this.setChoices(choices)
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(
this.formatState(this.state),
)
}
})
},
refreshChoices: async function (config = {}) {
this.setChoices(await this.getChoices(config))
},
setChoices: function (choices) {
this.select.setChoices(choices, 'value', 'label', true)
},
getChoices: async function (config = {}) {
const existingOptions = await this.getOptions(config)
return existingOptions.concat(
await this.getMissingOptions(existingOptions),
)
},
getOptions: async function ({ search, withInitialOptions }) {
if (withInitialOptions) {
return options
}
if (
search !== '' &&
search !== null &&
search !== undefined
) {
return await getSearchResultsUsing(search)
}
return await getOptionsUsing()
},
refreshPlaceholder: function () {
if (isMultiple) {
return
}
this.select._renderItems()
if (![null, undefined, ''].includes(this.state)) {
return
}
this.$el.querySelector(
'.choices__list--single',
).innerHTML = `<div class="choices__placeholder choices__item">${placeholder}</div>`
},
formatState: function (state) {
if (isMultiple) {
return (state ?? []).map((item) => item?.toString())
}
return state?.toString()
},
getMissingOptions: async function (existingOptions) {
let state = this.formatState(this.state)
if ([null, undefined, '', [], {}].includes(state)) {
return {}
}
const existingOptionValues = new Set(
existingOptions.length
? existingOptions.map((option) => option.value)
: [],
)
if (isMultiple) {
if (
state.every((value) =>
existingOptionValues.has(value),
)
) {
return {}
}
return await getOptionLabelsUsing()
}
if (existingOptionValues.has(state)) {
return existingOptionValues
}
return [
{
label: await getOptionLabelUsing(),
value: state,
},
]
},
this.isSearching = false
}, searchDebounce),
)
}
this.$watch('state', async () => {
this.refreshPlaceholder()
if (this.isStateBeingUpdated) {
return
}
const choices = await this.getChoices({
withInitialOptions: !hasDynamicOptions,
})
this.select.clearStore()
this.setChoices(choices)
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(
this.formatState(this.state),
)
}
})
},
)
refreshChoices: async function (config = {}) {
this.setChoices(await this.getChoices(config))
},
setChoices: function (choices) {
this.select.setChoices(choices, 'value', 'label', true)
},
getChoices: async function (config = {}) {
const existingOptions = await this.getOptions(config)
return existingOptions.concat(
await this.getMissingOptions(existingOptions),
)
},
getOptions: async function ({ search, withInitialOptions }) {
if (withInitialOptions) {
return options
}
if (
search !== '' &&
search !== null &&
search !== undefined
) {
return await getSearchResultsUsing(search)
}
return await getOptionsUsing()
},
refreshPlaceholder: function () {
if (isMultiple) {
return
}
this.select._renderItems()
if (![null, undefined, ''].includes(this.state)) {
return
}
this.$el.querySelector(
'.choices__list--single',
).innerHTML = `<div class="choices__placeholder choices__item">${placeholder}</div>`
},
formatState: function (state) {
if (isMultiple) {
return (state ?? []).map((item) => item?.toString())
}
return state?.toString()
},
getMissingOptions: async function (existingOptions) {
let state = this.formatState(this.state)
if ([null, undefined, '', [], {}].includes(state)) {
return {}
}
const existingOptionValues = new Set(
existingOptions.length
? existingOptions.map((option) => option.value)
: [],
)
if (isMultiple) {
if (
state.every((value) =>
existingOptionValues.has(value),
)
) {
return {}
}
return await getOptionLabelsUsing()
}
if (existingOptionValues.has(state)) {
return existingOptionValues
}
return [
{
label: await getOptionLabelUsing(),
value: state,
},
]
},
}
}
@@ -1,33 +1,29 @@
import '../../css/components/tags-input.css'
export default function tagsInputFormComponent ({ state }) {
return {
newTag: '',
export default (Alpine) => {
Alpine.data('tagsInputFormComponent', ({ state }) => {
return {
newTag: '',
state,
state,
createTag: function () {
this.newTag = this.newTag.trim()
createTag: function () {
this.newTag = this.newTag.trim()
if (this.newTag === '') {
return
}
if (this.state.includes(this.newTag)) {
this.newTag = ''
return
}
this.state.push(this.newTag)
if (this.newTag === '') {
return
}
if (this.state.includes(this.newTag)) {
this.newTag = ''
},
deleteTag: function (tagToDelete) {
this.state = this.state.filter((tag) => tag !== tagToDelete)
},
}
})
return
}
this.state.push(this.newTag)
this.newTag = ''
},
deleteTag: function (tagToDelete) {
this.state = this.state.filter((tag) => tag !== tagToDelete)
},
}
}
@@ -1,42 +1,40 @@
import IMask from 'imask'
export default (Alpine) => {
Alpine.data('textInputFormComponent', ({ getMaskOptionsUsing, state }) => {
return {
isStateBeingUpdated: false,
export default function textInputFormComponent({ getMaskOptionsUsing, state }) {
return {
isStateBeingUpdated: false,
mask: null,
mask: null,
state,
state,
init: function () {
if (!getMaskOptionsUsing) {
init: function () {
if (!getMaskOptionsUsing) {
return
}
if (this.state) {
this.$el.value = this.state?.valueOf()
}
this.mask = IMask(this.$el, getMaskOptionsUsing(IMask)).on(
'accept',
() => {
this.isStateBeingUpdated = true
this.state = this.mask.unmaskedValue
this.$nextTick(() => (this.isStateBeingUpdated = false))
},
)
this.$watch('state', () => {
if (this.isStateBeingUpdated) {
return
}
if (this.state) {
this.$el.value = this.state?.valueOf()
}
this.mask = IMask(this.$el, getMaskOptionsUsing(IMask)).on(
'accept',
() => {
this.isStateBeingUpdated = true
this.state = this.mask.unmaskedValue
this.$nextTick(() => (this.isStateBeingUpdated = false))
},
)
this.$watch('state', () => {
if (this.isStateBeingUpdated) {
return
}
this.mask.unmaskedValue = this.state?.valueOf() ?? ''
})
},
}
})
this.mask.unmaskedValue = this.state?.valueOf() ?? ''
})
},
}
}
@@ -1,5 +1,5 @@
export default (Alpine) => {
Alpine.data('textareaFormComponent', () => ({
export default function textareaFormComponent() {
return {
init: function () {
this.$nextTick(() => {
this.render()
@@ -12,5 +12,5 @@ export default (Alpine) => {
this.$el.style.height = this.$el.scrollHeight + 2 + 'px'
}
},
}))
}
}
+9 -127
View File
@@ -1,131 +1,13 @@
import ColorPickerFormComponentAlpinePlugin from './components/color-picker'
import DateTimePickerFormComponentAlpinePlugin from './components/date-time-picker'
import FileUploadFormComponentAlpinePlugin from './components/file-upload'
import KeyValueFormComponentAlpinePlugin from './components/key-value'
import MarkdownEditorFormComponentAlpinePlugin from './components/markdown-editor'
import RichEditorFormComponentAlpinePlugin from './components/rich-editor'
import SelectFormComponentAlpinePlugin from './components/select'
import TagsInputFormComponentAlpinePlugin from './components/tags-input'
import TextInputFormComponentAlpinePlugin from './components/text-input'
import TextareaFormComponentAlpinePlugin from './components/textarea'
import SortableAlpinePlugin from './sortable'
import AlpineFloatingUI from '@awcodes/alpine-floating-ui'
import '../css/components/color-picker.css'
// https://github.com/laravel/framework/blob/5299c22321c0f1ea8ff770b84a6c6469c4d6edec/src/Illuminate/Translation/MessageSelector.php#L15
window.pluralize = function (text, number, variables) {
function extract(segments, number) {
for (const part of segments) {
const line = extractFromString(part, number)
import 'filepond/dist/filepond.min.css'
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css'
import 'filepond-plugin-media-preview/dist/filepond-plugin-media-preview.css'
import '../css/components/file-upload.css'
if (line !== null) {
return line
}
}
}
import 'trix/dist/trix.css'
import '../css/components/rich-editor.css'
function extractFromString(part, number) {
const matches = part.match(/^[\{\[]([^\[\]\{\}]*)[\}\]](.*)/s)
import '../css/components/select.css'
if (matches === null || matches.length !== 3) {
return null
}
const condition = matches[1]
const value = matches[2]
if (condition.includes(',')) {
const [from, to] = condition.split(',', 2)
if (to === '*' && number >= from) {
return value
} else if (from === '*' && number <= to) {
return value
} else if (number >= from && number <= to) {
return value
}
}
return condition == number ? value : null
}
function ucfirst(string) {
return (
string.toString().charAt(0).toUpperCase() +
string.toString().slice(1)
)
}
function replace(line, replace) {
if (replace.length === 0) {
return line
}
const shouldReplace = {}
for (let [key, value] of Object.entries(replace)) {
shouldReplace[':' + ucfirst(key ?? '')] = ucfirst(value ?? '')
shouldReplace[':' + key.toUpperCase()] = value
.toString()
.toUpperCase()
shouldReplace[':' + key] = value
}
Object.entries(shouldReplace).forEach(([key, value]) => {
line = line.replaceAll(key, value)
})
return line
}
function stripConditions(segments) {
return segments.map((part) =>
part.replace(/^[\{\[]([^\[\]\{\}]*)[\}\]]/, ''),
)
}
let segments = text.split('|')
const value = extract(segments, number)
if (value !== null && value !== undefined) {
return replace(value.trim(), variables)
}
segments = stripConditions(segments)
return replace(
segments.length > 1 && number > 1 ? segments[1] : segments[0],
variables,
)
}
export default (Alpine) => {
Alpine.plugin(ColorPickerFormComponentAlpinePlugin)
Alpine.plugin(DateTimePickerFormComponentAlpinePlugin)
Alpine.plugin(FileUploadFormComponentAlpinePlugin)
Alpine.plugin(KeyValueFormComponentAlpinePlugin)
Alpine.plugin(MarkdownEditorFormComponentAlpinePlugin)
Alpine.plugin(RichEditorFormComponentAlpinePlugin)
Alpine.plugin(SelectFormComponentAlpinePlugin)
Alpine.plugin(TagsInputFormComponentAlpinePlugin)
Alpine.plugin(TextInputFormComponentAlpinePlugin)
Alpine.plugin(TextareaFormComponentAlpinePlugin)
Alpine.plugin(SortableAlpinePlugin)
Alpine.plugin(AlpineFloatingUI)
}
export {
ColorPickerFormComponentAlpinePlugin,
DateTimePickerFormComponentAlpinePlugin,
FileUploadFormComponentAlpinePlugin,
KeyValueFormComponentAlpinePlugin,
MarkdownEditorFormComponentAlpinePlugin,
RichEditorFormComponentAlpinePlugin,
SelectFormComponentAlpinePlugin,
TagsInputFormComponentAlpinePlugin,
TextInputFormComponentAlpinePlugin,
TextareaFormComponentAlpinePlugin,
SortableAlpinePlugin,
AlpineFloatingUI,
}
import '../css/components/tags-input.css'
@@ -35,6 +35,9 @@
@endif
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/color-picker.js"
x-data="colorPickerFormComponent({
isAutofocused: @js($isAutofocused()),
isDisabled: @js($isDisabled()),
@@ -12,6 +12,9 @@
:state-path="$getStatePath()"
>
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/date-time-picker.js"
x-data="dateTimePickerFormComponent({
displayFormat: '{{ convert_date_format($getDisplayFormat())->to('day.js') }}',
firstDayOfWeek: {{ $getFirstDayOfWeek() }},
@@ -19,6 +19,9 @@
$shouldTransformImage = $imageCropAspectRatio || $imageResizeTargetHeight || $imageResizeTargetWidth;
@endphp
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/file-upload.js"
x-data="fileUploadFormComponent({
acceptedFileTypes: {{ json_encode($getAcceptedFileTypes()) }},
canDownload: {{ $canDownload() ? 'true' : 'false' }},
@@ -12,6 +12,9 @@
:state-path="$getStatePath()"
>
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/key-value.js"
x-data="keyValueFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
@@ -12,6 +12,9 @@
:state-path="$getStatePath()"
>
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/markdown-editor.js"
x-data="markdownEditorFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
tab: '{{ $isDisabled() ? 'preview' : 'edit' }}',
@@ -12,6 +12,9 @@
:state-path="$getStatePath()"
>
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/rich-editor.js"
x-data="richEditorFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
@@ -68,6 +68,9 @@
</select>
@else
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/select.js"
x-data="selectFormComponent({
isHtmlAllowed: @js($isHtmlAllowed()),
getOptionLabelUsing: async () => {
@@ -12,6 +12,9 @@
:state-path="$getStatePath()"
>
<div
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/tags-input.js"
x-data="tagsInputFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
})"
@@ -43,6 +43,9 @@
{{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
type="{{ $getType() }}"
@else
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/text-input.js"
x-data="textInputFormComponent({
{{ $hasMask() ? "getMaskOptionsUsing: (IMask) => ({$getJsonMaskConfiguration()})," : null }}
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')', lazilyEntangledModifiers: ['defer']) }},
@@ -39,6 +39,9 @@
])
}}
@if ($shouldAutosize())
x-ignore
ax-load="visible"
ax-load-src="/js/filament/forms/components/textarea.js"
x-data="textareaFormComponent()"
x-on:input="render()"
style="height: 150px"
@@ -3,6 +3,9 @@
namespace Filament\Forms;
use Filament\Forms\Testing\TestsForms;
use Filament\Support\Assets\AlpineComponent;
use Filament\Support\Assets\Js;
use Filament\Support\Facades\Asset;
use Illuminate\Filesystem\Filesystem;
use Livewire\Testing\TestableLivewire;
use Spatie\LaravelPackageTools\Package;
@@ -45,6 +48,19 @@ class FormsServiceProvider extends PackageServiceProvider
public function packageBooted(): void
{
Asset::register([
AlpineComponent::make('color-picker', __DIR__ . '/../dist/components/color-picker.js'),
AlpineComponent::make('date-time-picker', __DIR__ . '/../dist/components/date-time-picker.js'),
AlpineComponent::make('file-upload', __DIR__ . '/../dist/components/file-upload.js'),
AlpineComponent::make('key-value', __DIR__ . '/../dist/components/key-value.js'),
AlpineComponent::make('markdown-editor', __DIR__ . '/../dist/components/markdown-editor.js'),
AlpineComponent::make('rich-editor', __DIR__ . '/../dist/components/rich-editor.js'),
AlpineComponent::make('select', __DIR__ . '/../dist/components/select.js'),
AlpineComponent::make('tags-input', __DIR__ . '/../dist/components/tags-input.js'),
AlpineComponent::make('text-input', __DIR__ . '/../dist/components/text-input.js'),
AlpineComponent::make('textarea', __DIR__ . '/../dist/components/textarea.js'),
], 'forms');
if ($this->app->runningInConsole()) {
foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) {
$this->publishes([
File diff suppressed because one or more lines are too long
+3 -10
View File
@@ -9,13 +9,6 @@ window.NotificationAction = NotificationAction
window.NotificationActionGroup = NotificationActionGroup
window.Notification = Notification
export default (Alpine) => {
Alpine.plugin(NotificationComponentAlpinePlugin)
}
export {
NotificationAction,
NotificationActionGroup,
Notification,
NotificationComponentAlpinePlugin,
}
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(NotificationComponentAlpinePlugin)
})
@@ -4,6 +4,8 @@ namespace Filament\Notifications;
use Filament\Notifications\Http\Livewire\Notifications;
use Filament\Notifications\Testing\TestsNotifications;
use Filament\Support\Assets\Js;
use Filament\Support\Facades\Asset;
use Livewire\Component;
use Livewire\Livewire;
use Livewire\Response;
@@ -25,6 +27,10 @@ class NotificationsServiceProvider extends PackageServiceProvider
public function packageBooted(): void
{
Asset::register([
Js::make('index', __DIR__ . '/../dist/index.js'),
], 'notifications');
Livewire::component('notifications', Notifications::class);
Livewire::listen('component.dehydrate', function (Component $component, Response $response): Response {
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
import SortableAlpinePlugin from './sortable'
import AlpineFloatingUI from '@awcodes/alpine-floating-ui'
import AsyncAlpine from 'async-alpine/dist/async-alpine.esm'
// https://github.com/laravel/framework/blob/5299c22321c0f1ea8ff770b84a6c6469c4d6edec/src/Illuminate/Translation/MessageSelector.php#L15
const pluralize = function (text, number, variables) {
function extract(segments, number) {
for (const part of segments) {
const line = extractFromString(part, number)
if (line !== null) {
return line
}
}
}
function extractFromString(part, number) {
const matches = part.match(/^[\{\[]([^\[\]\{\}]*)[\}\]](.*)/s)
if (matches === null || matches.length !== 3) {
return null
}
const condition = matches[1]
const value = matches[2]
if (condition.includes(',')) {
const [from, to] = condition.split(',', 2)
if (to === '*' && number >= from) {
return value
} else if (from === '*' && number <= to) {
return value
} else if (number >= from && number <= to) {
return value
}
}
return condition == number ? value : null
}
function ucfirst(string) {
return (
string.toString().charAt(0).toUpperCase() +
string.toString().slice(1)
)
}
function replace(line, replace) {
if (replace.length === 0) {
return line
}
const shouldReplace = {}
for (let [key, value] of Object.entries(replace)) {
shouldReplace[':' + ucfirst(key ?? '')] = ucfirst(value ?? '')
shouldReplace[':' + key.toUpperCase()] = value
.toString()
.toUpperCase()
shouldReplace[':' + key] = value
}
Object.entries(shouldReplace).forEach(([key, value]) => {
line = line.replaceAll(key, value)
})
return line
}
function stripConditions(segments) {
return segments.map((part) =>
part.replace(/^[\{\[]([^\[\]\{\}]*)[\}\]]/, ''),
)
}
let segments = text.split('|')
const value = extract(segments, number)
if (value !== null && value !== undefined) {
return replace(value.trim(), variables)
}
segments = stripConditions(segments)
return replace(
segments.length > 1 && number > 1 ? segments[1] : segments[0],
variables,
)
}
window.pluralize = pluralize
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(SortableAlpinePlugin)
window.Alpine.plugin(AlpineFloatingUI)
AsyncAlpine.init(window.Alpine)
AsyncAlpine.start()
})
@@ -0,0 +1,8 @@
@foreach ($assets as $asset)
<script
src="{{ $asset->getSrc() }}"
@if ($asset->isAsync()) async @endif
@if ($asset->isDeferred()) defer @endif
></script>
@endforeach
@@ -0,0 +1,3 @@
@foreach ($assets as $asset)
<link rel="stylesheet" href="{{ $asset->getHref() }}" />
@endforeach
@@ -0,0 +1,24 @@
<?php
namespace Filament\Support\Assets;
class AlpineComponent extends Asset
{
public function getPublicPath(): string
{
$path = '';
$package = $this->getPackage();
if (filled($package)) {
$path .= $package;
$path .= DIRECTORY_SEPARATOR;
}
$path .= 'components';
$path .= DIRECTORY_SEPARATOR;
$path .= "{$this->getName()}.js";
return public_path("js/filament/{$path}");
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Filament\Support\Assets;
abstract class Asset
{
protected string $name;
protected string $path;
protected ?string $package = null;
final public function __construct(string $name, string $path)
{
$this->name = $name;
$this->path = $path;
}
public static function make(string $name, string $path): static
{
return new static($name, $path);
}
public function getName(): string
{
return $this->name;
}
public function package(?string $package): static
{
$this->package = $package;
return $this;
}
public function getPackage(): ?string
{
return $this->package;
}
public function getPath(): string
{
return $this->path;
}
abstract public function getPublicPath(): string;
}
@@ -0,0 +1,71 @@
<?php
namespace Filament\Support\Assets;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
class AssetManager
{
protected array $alpineComponents = [];
protected array $scripts = [];
protected array $styles = [];
public function register(array $assets, ?string $package = null): static
{
foreach ($assets as $asset) {
$asset->package($package);
match ($asset::class) {
AlpineComponent::class => $this->alpineComponents[$package][] = $asset,
Css::class => $this->styles[$package][] = $asset,
Js::class => $this->scripts[$package][] = $asset,
};
}
return $this;
}
public function getAlpineComponents(?array $packages = null): array
{
return $this->getAssets($this->alpineComponents, $packages);
}
public function getScripts(?array $packages = null): array
{
return $this->getAssets($this->scripts, $packages);
}
public function renderScripts(?array $packages = null): string
{
return view('filament-support::assets.scripts', [
'assets' => $this->getScripts($packages),
])->render();
}
public function getStyles(?array $packages = null): array
{
return $this->getAssets($this->styles, $packages);
}
public function renderStyles(?array $packages = null): string
{
return view('filament-support::assets.styles', [
'assets' => $this->getStyles($packages),
])->render();
}
protected function getAssets(array $assets, ?array $packages = null): array
{
if ($packages === null) {
return Arr::flatten($assets);
}
$packages[] = null;
return Arr::only($assets, $packages);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Filament\Support\Assets;
class Css extends Asset
{
public function getHref(): string
{
$href = '/css/filament/';
$package = $this->getPackage();
if (filled($package)) {
$href .= "{$package}/";
}
$href .= "{$this->getName()}.css";
return $href;
}
public function getPublicPath(): string
{
$path = '';
$package = $this->getPackage();
if (filled($package)) {
$path .= $package;
$path .= DIRECTORY_SEPARATOR;
}
$path .= "{$this->getName()}.css";
return public_path("css/filament/{$path}");
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Filament\Support\Assets;
class Js extends Asset
{
protected bool $isAsync = false;
protected bool $isDeferred = true;
public function async(bool $condition = true): static
{
$this->isAsync = $condition;
return $this;
}
public function defer(bool $condition = true): static
{
$this->isDeferred = $condition;
return $this;
}
public function isAsync(): bool
{
return $this->isAsync;
}
public function isDeferred(): bool
{
return $this->isDeferred;
}
public function getSrc(): string
{
$href = '/js/filament/';
$package = $this->getPackage();
if (filled($package)) {
$href .= "{$package}/";
}
$href .= "{$this->getName()}.js";
return $href;
}
public function getPublicPath(): string
{
$path = '';
$package = $this->getPackage();
if (filled($package)) {
$path .= $package;
$path .= DIRECTORY_SEPARATOR;
}
$path .= "{$this->getName()}.js";
return public_path("js/filament/{$path}");
}
}
@@ -0,0 +1,49 @@
<?php
namespace Filament\Support\Commands;
use Filament\Support\Commands\Concerns\CanManipulateFiles;
use Filament\Support\Facades\Asset;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class AssetsCommand extends Command
{
use CanManipulateFiles;
protected $description = 'Set up Filament assets.';
protected $signature = 'filament:assets';
public function handle(): int
{
foreach (Asset::getAlpineComponents() as $asset) {
$this->copyAsset($asset->getPath(), $asset->getPublicPath());
}
foreach (Asset::getScripts() as $asset) {
$this->copyAsset($asset->getPath(), $asset->getPublicPath());
}
foreach (Asset::getStyles() as $asset) {
$this->copyAsset($asset->getPath(), $asset->getPublicPath());
}
$this->components->info('Successfully published assets!');
return static::SUCCESS;
}
protected function copyAsset(string $from, string $to): void
{
$filesystem = app(Filesystem::class);
$filesystem->ensureDirectoryExists(
(string) Str::of($to)
->beforeLast(DIRECTORY_SEPARATOR),
);
$filesystem->copy($from, $to);
}
}
@@ -13,6 +13,7 @@ class UpgradeCommand extends Command
public function handle(): int
{
foreach ([
'filament:assets',
'config:clear',
'livewire:discover',
'route:clear',
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Filament\Support\Facades;
use Filament\Support\Assets\AssetManager;
use Illuminate\Support\Facades\Facade;
class Asset extends Facade
{
protected static function getFacadeAccessor()
{
return AssetManager::class;
}
}
@@ -3,8 +3,12 @@
namespace Filament\Support;
use Composer\InstalledVersions;
use Filament\Support\Assets\AssetManager;
use Filament\Support\Assets\Js;
use Filament\Support\Commands\AssetsCommand;
use Filament\Support\Commands\CheckTranslationsCommand;
use Filament\Support\Commands\UpgradeCommand;
use Filament\Support\Facades\Asset;
use Filament\Support\Testing\TestsActions;
use HtmlSanitizer\Sanitizer;
use HtmlSanitizer\SanitizerInterface;
@@ -23,6 +27,7 @@ class SupportServiceProvider extends PackageServiceProvider
$package
->name('filament-support')
->hasCommands([
AssetsCommand::class,
CheckTranslationsCommand::class,
UpgradeCommand::class,
])
@@ -33,6 +38,13 @@ class SupportServiceProvider extends PackageServiceProvider
public function packageRegistered()
{
$this->app->scoped(
AssetManager::class,
function () {
return new AssetManager();
},
);
$this->app->scoped(
SanitizerInterface::class,
function () {
@@ -45,10 +57,22 @@ class SupportServiceProvider extends PackageServiceProvider
public function packageBooted()
{
Asset::register([
Js::make('index', __DIR__ . '/../dist/index.js'),
]);
Blade::directive('captureSlots', function (string $expression): string {
return "<?php \$slotContents = get_defined_vars(); \$slots = collect({$expression})->mapWithKeys(fn (string \$slot): array => [\$slot => \$slotContents[\$slot] ?? null])->all(); unset(\$slotContents) ?>";
});
Blade::directive('filamentScripts', function (string $expression): string {
return "<?php echo \Filament\Support\Facades\Asset::renderScripts({$expression}) ?>";
});
Blade::directive('filamentStyles', function (string $expression): string {
return "<?php echo \Filament\Support\Facades\Asset::renderStyles({$expression}) ?>";
});
Str::macro('lcfirst', function (string $string): string {
return Str::lower(Str::substr($string, 0, 1)) . Str::substr($string, 1);
});