🆙 Add cms i using 🆙

This commit is contained in:
Remco
2025-11-25 22:42:56 +01:00
parent 94704e0925
commit d44196149e
35591 changed files with 3601123 additions and 0 deletions
@@ -0,0 +1,232 @@
export default function filamentTableColumnManager({ columns, isLive }) {
return {
error: undefined,
isLoading: false,
deferredColumns: [],
columns,
isLive,
hasReordered: false,
init() {
if (!this.columns || this.columns.length === 0) {
this.columns = []
return
}
this.deferredColumns = JSON.parse(JSON.stringify(this.columns))
},
get groupedColumns() {
const groupedColumns = {}
this.deferredColumns
.filter((column) => column.type === 'group')
.forEach((column) => {
groupedColumns[column.name] =
this.calculateGroupedColumns(column)
})
return groupedColumns
},
calculateGroupedColumns(group) {
const visibleChildren =
group?.columns?.filter((column) => !column.isHidden) ?? []
if (visibleChildren.length === 0) {
return {
hidden: true,
checked: false,
disabled: false,
indeterminate: false,
}
}
const toggleableChildren = group.columns.filter(
(column) => !column.isHidden && column.isToggleable !== false,
)
if (toggleableChildren.length === 0) {
return { checked: true, disabled: true, indeterminate: false }
}
const toggledChildren = toggleableChildren.filter(
(column) => column.isToggled,
).length
const nonToggleableChildren = group.columns.filter(
(column) => !column.isHidden && column.isToggleable === false,
)
if (toggledChildren === 0 && nonToggleableChildren.length > 0) {
return { checked: true, disabled: false, indeterminate: true }
}
if (toggledChildren === 0) {
return { checked: false, disabled: false, indeterminate: false }
}
if (toggledChildren === toggleableChildren.length) {
return { checked: true, disabled: false, indeterminate: false }
}
return { checked: true, disabled: false, indeterminate: true }
},
getColumn(name, groupName = null) {
if (groupName) {
const group = this.deferredColumns.find(
(group) =>
group.type === 'group' && group.name === groupName,
)
return group?.columns?.find((column) => column.name === name)
}
return this.deferredColumns.find((column) => column.name === name)
},
toggleGroup(groupName) {
const group = this.deferredColumns.find(
(group) => group.type === 'group' && group.name === groupName,
)
if (!group?.columns) {
return
}
const groupedColumns = this.calculateGroupedColumns(group)
if (groupedColumns.disabled) {
return
}
const toggleableChildren = group.columns.filter(
(column) => column.isToggleable !== false,
)
const anyChildOn = toggleableChildren.some(
(column) => column.isToggled,
)
const newValue = groupedColumns.indeterminate ? true : !anyChildOn
group.columns
.filter((column) => column.isToggleable !== false)
.forEach((column) => {
column.isToggled = newValue
})
this.deferredColumns = [...this.deferredColumns]
if (this.isLive) {
this.applyTableColumnManager()
}
},
toggleColumn(name, groupName = null) {
const column = this.getColumn(name, groupName)
if (!column || column.isToggleable === false) {
return
}
column.isToggled = !column.isToggled
this.deferredColumns = [...this.deferredColumns]
if (this.isLive) {
this.applyTableColumnManager()
}
},
reorderColumns(sortedIds) {
const newOrder = sortedIds.map((id) => id.split('::'))
this.reorderTopLevel(newOrder)
this.hasReordered = true
if (this.isLive) {
this.applyTableColumnManager()
}
},
reorderGroupColumns(sortedIds, groupName) {
const group = this.deferredColumns.find(
(column) =>
column.type === 'group' && column.name === groupName,
)
if (!group) {
return
}
const newOrder = sortedIds.map((id) => id.split('::'))
const reordered = []
newOrder.forEach(([type, name]) => {
const item = group.columns.find(
(column) => column.name === name,
)
if (item) {
reordered.push(item)
}
})
group.columns = reordered
this.deferredColumns = [...this.deferredColumns]
this.hasReordered = true
if (this.isLive) {
this.applyTableColumnManager()
}
},
reorderTopLevel(newOrder) {
const cloned = this.deferredColumns
const reordered = []
newOrder.forEach(([type, name]) => {
const item = cloned.find((column) => {
if (type === 'group') {
return column.type === 'group' && column.name === name
} else if (type === 'column') {
return column.type !== 'group' && column.name === name
}
return false
})
if (item) {
reordered.push(item)
}
})
this.deferredColumns = reordered
},
async applyTableColumnManager() {
this.isLoading = true
try {
this.columns = JSON.parse(JSON.stringify(this.deferredColumns))
await this.$wire.call(
'applyTableColumnManager',
this.columns,
this.hasReordered,
)
this.hasReordered = false
this.error = undefined
} catch (error) {
this.error = 'Failed to update column visibility'
console.error('Table toggle columns error:', error)
} finally {
this.isLoading = false
}
},
}
}
@@ -0,0 +1,79 @@
export default function checkboxTableColumn({ name, recordKey, state }) {
return {
error: undefined,
isLoading: false,
state,
init() {
Livewire.hook(
'commit',
({ component, commit, succeed, fail, respond }) => {
succeed(({ snapshot, effect }) => {
this.$nextTick(() => {
if (this.isLoading) {
return
}
if (
component.id !==
this.$root.closest('[wire\\:id]')?.attributes[
'wire:id'
].value
) {
return
}
const serverState = this.getServerState()
if (
serverState === undefined ||
Alpine.raw(this.state) === serverState
) {
return
}
this.state = serverState
})
})
},
)
this.$watch('state', async () => {
const serverState = this.getServerState()
if (
serverState === undefined ||
Alpine.raw(this.state) === serverState
) {
return
}
this.isLoading = true
const response = await this.$wire.updateTableColumnState(
name,
recordKey,
this.state,
)
this.error = response?.error ?? undefined
if (!this.error && this.$refs.serverState) {
this.$refs.serverState.value = this.state ? '1' : '0'
}
this.isLoading = false
})
},
getServerState() {
if (!this.$refs.serverState) {
return undefined
}
return [1, '1'].includes(this.$refs.serverState.value)
},
}
}
@@ -0,0 +1,172 @@
import { Select } from '../../../../../support/resources/js/utilities/select.js'
export default function selectTableColumn({
canOptionLabelsWrap,
canSelectPlaceholder,
getOptionLabelUsing,
getOptionsUsing,
getSearchResultsUsing,
hasDynamicOptions,
hasDynamicSearchResults,
initialOptionLabel,
isDisabled,
isHtmlAllowed,
isNative,
isSearchable,
loadingMessage,
name,
noSearchResultsMessage,
options,
optionsLimit,
placeholder,
position,
recordKey,
searchableOptionFields,
searchDebounce,
searchingMessage,
searchPrompt,
state,
}) {
return {
error: undefined,
isLoading: false,
select: null,
state,
init() {
if (!isNative) {
this.select = new Select({
element: this.$refs.select,
options,
placeholder,
state: this.state,
canOptionLabelsWrap,
canSelectPlaceholder,
initialOptionLabel,
isHtmlAllowed,
isDisabled,
isSearchable,
getOptionLabelUsing,
getOptionsUsing,
getSearchResultsUsing,
hasDynamicOptions,
hasDynamicSearchResults,
searchPrompt,
searchDebounce,
loadingMessage,
searchingMessage,
noSearchResultsMessage,
optionsLimit,
position,
searchableOptionFields,
onStateChange: (newState) => {
this.state = newState
},
})
}
Livewire.hook(
'commit',
({ component, commit, succeed, fail, respond }) => {
succeed(({ snapshot, effect }) => {
this.$nextTick(() => {
if (this.isLoading) {
return
}
if (
component.id !==
this.$root.closest('[wire\\:id]')?.attributes[
'wire:id'
].value
) {
return
}
const serverState = this.getServerState()
if (
serverState === undefined ||
this.getNormalizedState() === serverState
) {
return
}
this.state = serverState
})
})
},
)
this.$watch('state', async (newState) => {
if (
!isNative &&
this.select &&
this.select.state !== newState
) {
this.select.state = newState
this.select.updateSelectedDisplay()
this.select.renderOptions()
}
const serverState = this.getServerState()
if (
serverState === undefined ||
this.getNormalizedState() === serverState
) {
return
}
this.isLoading = true
const response = await this.$wire.updateTableColumnState(
name,
recordKey,
this.state,
)
this.error = response?.error ?? undefined
if (!this.error && this.$refs.serverState) {
this.$refs.serverState.value = this.getNormalizedState()
}
this.isLoading = false
})
},
getServerState() {
if (!this.$refs.serverState) {
return undefined
}
return [null, undefined].includes(this.$refs.serverState.value)
? ''
: this.$refs.serverState.value.replaceAll(
'\\' + String.fromCharCode(34),
String.fromCharCode(34),
)
},
getNormalizedState() {
const state = Alpine.raw(this.state)
if ([null, undefined].includes(state)) {
return ''
}
return state
},
destroy() {
if (this.select) {
this.select.destroy()
this.select = null
}
},
}
}
@@ -0,0 +1,94 @@
export default function textInputTableColumn({ name, recordKey, state }) {
return {
error: undefined,
isLoading: false,
state,
init() {
Livewire.hook(
'commit',
({ component, commit, succeed, fail, respond }) => {
succeed(({ snapshot, effect }) => {
this.$nextTick(() => {
if (this.isLoading) {
return
}
if (
component.id !==
this.$root.closest('[wire\\:id]')?.attributes[
'wire:id'
].value
) {
return
}
const serverState = this.getServerState()
if (
serverState === undefined ||
this.getNormalizedState() === serverState
) {
return
}
this.state = serverState
})
})
},
)
this.$watch('state', async () => {
const serverState = this.getServerState()
if (
serverState === undefined ||
this.getNormalizedState() === serverState
) {
return
}
this.isLoading = true
const response = await this.$wire.updateTableColumnState(
name,
recordKey,
this.state,
)
this.error = response?.error ?? undefined
if (!this.error && this.$refs.serverState) {
this.$refs.serverState.value = this.getNormalizedState()
}
this.isLoading = false
})
},
getServerState() {
if (!this.$refs.serverState) {
return undefined
}
return [null, undefined].includes(this.$refs.serverState.value)
? ''
: this.$refs.serverState.value.replaceAll(
'\\' + String.fromCharCode(34),
String.fromCharCode(34),
)
},
getNormalizedState() {
const state = Alpine.raw(this.state)
if ([null, undefined].includes(state)) {
return ''
}
return state
},
}
}
@@ -0,0 +1,79 @@
export default function toggleTableColumn({ name, recordKey, state }) {
return {
error: undefined,
isLoading: false,
state,
init() {
Livewire.hook(
'commit',
({ component, commit, succeed, fail, respond }) => {
succeed(({ snapshot, effect }) => {
this.$nextTick(() => {
if (this.isLoading) {
return
}
if (
component.id !==
this.$root.closest('[wire\\:id]')?.attributes[
'wire:id'
].value
) {
return
}
const serverState = this.getServerState()
if (
serverState === undefined ||
Alpine.raw(this.state) === serverState
) {
return
}
this.state = serverState
})
})
},
)
this.$watch('state', async () => {
const serverState = this.getServerState()
if (
serverState === undefined ||
Alpine.raw(this.state) === serverState
) {
return
}
this.isLoading = true
const response = await this.$wire.updateTableColumnState(
name,
recordKey,
this.state,
)
this.error = response?.error ?? undefined
if (!this.error && this.$refs.serverState) {
this.$refs.serverState.value = this.state ? '1' : '0'
}
this.isLoading = false
})
},
getServerState() {
if (!this.$refs.serverState) {
return undefined
}
return [1, '1'].includes(this.$refs.serverState.value)
},
}
}
@@ -0,0 +1,439 @@
import { autoUpdate, computePosition, offset, shift } from '@floating-ui/dom'
export default ({
areGroupsCollapsedByDefault,
canTrackDeselectedRecords,
currentSelectionLivewireProperty,
maxSelectableRecords,
selectsCurrentPageOnly,
$wire,
}) => ({
areFiltersOpen: false,
checkboxClickController: null,
groupVisibility: [],
isLoading: false,
selectedRecords: new Set(),
deselectedRecords: new Set(),
isTrackingDeselectedRecords: false,
shouldCheckUniqueSelection: true,
lastCheckedRecord: null,
livewireId: null,
entangledSelectedRecords: currentSelectionLivewireProperty
? $wire.$entangle(currentSelectionLivewireProperty)
: null,
cleanUpFiltersDropdown: null,
init() {
this.livewireId =
this.$root.closest('[wire\\:id]')?.attributes['wire:id'].value
$wire.$on('deselectAllTableRecords', () => this.deselectAllRecords())
$wire.$on('scrollToTopOfTable', () =>
this.$root.scrollIntoView({ block: 'start', inline: 'nearest' }),
)
if (currentSelectionLivewireProperty) {
if (maxSelectableRecords !== 1) {
this.selectedRecords = new Set(this.entangledSelectedRecords)
} else {
this.selectedRecords = new Set(
this.entangledSelectedRecords
? [this.entangledSelectedRecords]
: [],
)
}
}
this.$nextTick(() => this.watchForCheckboxClicks())
Livewire.hook('element.init', ({ component }) => {
if (component.id === this.livewireId) {
this.watchForCheckboxClicks()
}
})
},
mountAction(...args) {
$wire.set(
'isTrackingDeselectedTableRecords',
this.isTrackingDeselectedRecords,
false,
)
$wire.set('selectedTableRecords', [...this.selectedRecords], false)
$wire.set('deselectedTableRecords', [...this.deselectedRecords], false)
$wire.mountAction(...args)
},
toggleSelectRecordsOnPage() {
const keys = this.getRecordsOnPage()
if (this.areRecordsSelected(keys)) {
this.deselectRecords(keys)
return
}
this.selectRecords(keys)
},
toggleSelectRecords(keys) {
if (this.areRecordsSelected(keys)) {
this.deselectRecords(keys)
} else {
this.selectRecords(keys)
}
},
getSelectedRecordsCount() {
if (this.isTrackingDeselectedRecords) {
return (
(this.$refs.allSelectableRecordsCount?.value ??
this.deselectedRecords.size) - this.deselectedRecords.size
)
}
return this.selectedRecords.size
},
getRecordsOnPage() {
const keys = []
for (let checkbox of this.$root?.getElementsByClassName(
'fi-ta-record-checkbox',
) ?? []) {
keys.push(checkbox.value)
}
return keys
},
selectRecords(keys) {
if (maxSelectableRecords === 1) {
this.deselectAllRecords()
keys = keys.slice(0, 1)
}
for (let key of keys) {
if (this.isRecordSelected(key)) {
continue
}
if (this.isTrackingDeselectedRecords) {
this.deselectedRecords.delete(key)
continue
}
this.selectedRecords.add(key)
}
this.updatedSelectedRecords()
},
deselectRecords(keys) {
for (let key of keys) {
if (this.isTrackingDeselectedRecords) {
this.deselectedRecords.add(key)
continue
}
this.selectedRecords.delete(key)
}
this.updatedSelectedRecords()
},
updatedSelectedRecords() {
if (maxSelectableRecords !== 1) {
this.entangledSelectedRecords = [...this.selectedRecords]
return
}
this.entangledSelectedRecords = [...this.selectedRecords][0] ?? null
},
toggleSelectedRecord(key) {
if (this.isRecordSelected(key)) {
this.deselectRecords([key])
return
}
this.selectRecords([key])
},
async selectAllRecords() {
if (!canTrackDeselectedRecords || selectsCurrentPageOnly) {
this.isLoading = true
this.selectedRecords = new Set(
await $wire.getAllSelectableTableRecordKeys(),
)
this.updatedSelectedRecords()
this.isLoading = false
return
}
this.isTrackingDeselectedRecords = true
this.selectedRecords = new Set()
this.deselectedRecords = new Set()
this.updatedSelectedRecords()
},
canSelectAllRecords() {
if (selectsCurrentPageOnly) {
const recordsOnPage = this.getRecordsOnPage()
return (
!this.areRecordsSelected(recordsOnPage) &&
this.areRecordsToggleable(recordsOnPage)
)
}
const allSelectableRecordsCount = parseInt(
this.$refs.allSelectableRecordsCount?.value,
)
if (!allSelectableRecordsCount) {
return false
}
const selectedRecordsCount = this.getSelectedRecordsCount()
if (allSelectableRecordsCount === selectedRecordsCount) {
return false
}
return (
maxSelectableRecords === null ||
allSelectableRecordsCount <= maxSelectableRecords
)
},
deselectAllRecords() {
this.isTrackingDeselectedRecords = false
this.selectedRecords = new Set()
this.deselectedRecords = new Set()
this.updatedSelectedRecords()
},
isRecordSelected(key) {
if (this.isTrackingDeselectedRecords) {
return !this.deselectedRecords.has(key)
}
return this.selectedRecords.has(key)
},
areRecordsSelected(keys) {
return keys.every((key) => this.isRecordSelected(key))
},
areRecordsToggleable(keys) {
if (maxSelectableRecords === null) {
return true
}
if (maxSelectableRecords === 1) {
return true
}
const selectedRecords = keys.filter((key) => this.isRecordSelected(key))
if (selectedRecords.length === keys.length) {
return true
}
return (
this.getSelectedRecordsCount() +
(keys.length - selectedRecords.length) <=
maxSelectableRecords
)
},
toggleCollapseGroup(group) {
if (this.isGroupCollapsed(group)) {
if (areGroupsCollapsedByDefault) {
this.groupVisibility.push(group)
} else {
this.groupVisibility.splice(
this.groupVisibility.indexOf(group),
1,
)
}
} else {
if (areGroupsCollapsedByDefault) {
this.groupVisibility.splice(
this.groupVisibility.indexOf(group),
1,
)
} else {
this.groupVisibility.push(group)
}
}
},
isGroupCollapsed(group) {
if (areGroupsCollapsedByDefault) {
return !this.groupVisibility.includes(group)
}
return this.groupVisibility.includes(group)
},
resetCollapsedGroups() {
this.groupVisibility = []
},
watchForCheckboxClicks() {
if (this.checkboxClickController) {
this.checkboxClickController.abort()
}
this.checkboxClickController = new AbortController()
const { signal } = this.checkboxClickController
this.$root?.addEventListener(
'click',
(event) =>
event.target?.matches('.fi-ta-record-checkbox') &&
this.handleCheckboxClick(event, event.target),
{ signal },
)
},
handleCheckboxClick(event, checkbox) {
if (!this.lastChecked) {
this.lastChecked = checkbox
return
}
if (event.shiftKey) {
let checkboxes = Array.from(
this.$root?.getElementsByClassName('fi-ta-record-checkbox') ??
[],
)
if (!checkboxes.includes(this.lastChecked)) {
this.lastChecked = checkbox
return
}
let start = checkboxes.indexOf(this.lastChecked)
let end = checkboxes.indexOf(checkbox)
let range = [start, end].sort((a, b) => a - b)
let values = []
for (let i = range[0]; i <= range[1]; i++) {
values.push(checkboxes[i].value)
}
if (checkbox.checked) {
if (!this.areRecordsToggleable(values)) {
checkbox.checked = false
this.deselectRecords([checkbox.value])
return
}
this.selectRecords(values)
} else {
this.deselectRecords(values)
}
}
this.lastChecked = checkbox
},
toggleFiltersDropdown() {
this.areFiltersOpen = !this.areFiltersOpen
if (this.areFiltersOpen) {
const cleanUpAutoUpdate = autoUpdate(
this.$refs.filtersTriggerActionContainer,
this.$refs.filtersContentContainer,
async () => {
const { x, y } = await computePosition(
this.$refs.filtersTriggerActionContainer,
this.$refs.filtersContentContainer,
{
placement: 'bottom-end',
middleware: [offset(8), shift({ padding: 8 })],
},
)
Object.assign(this.$refs.filtersContentContainer.style, {
left: `${x}px`,
top: `${y}px`,
})
},
)
const onClickAway = (event) => {
const trigger = this.$refs.filtersTriggerActionContainer
const filters = this.$refs.filtersContentContainer
if (
(filters && filters.contains(event.target)) ||
(trigger && trigger.contains(event.target))
) {
return
}
this.areFiltersOpen = false
if (this.cleanUpFiltersDropdown) {
this.cleanUpFiltersDropdown()
this.cleanUpFiltersDropdown = null
}
}
document.addEventListener('mousedown', onClickAway)
document.addEventListener('touchstart', onClickAway, {
passive: true,
})
const onKeydown = (event) => {
if (event.key === 'Escape') {
onClickAway(event)
}
}
document.addEventListener('keydown', onKeydown)
this.cleanUpFiltersDropdown = () => {
cleanUpAutoUpdate()
document.removeEventListener('mousedown', onClickAway)
document.removeEventListener('touchstart', onClickAway, {
passive: true,
})
document.removeEventListener('keydown', onKeydown)
}
} else if (this.cleanUpFiltersDropdown) {
this.cleanUpFiltersDropdown()
this.cleanUpFiltersDropdown = null
}
},
})
@@ -0,0 +1,7 @@
import table from './components/table.js'
import columnManager from './components/column-manager.js'
document.addEventListener('alpine:init', () => {
window.Alpine.data('filamentTable', table)
window.Alpine.data('filamentTableColumnManager', columnManager)
})