Release v2.2.0
This commit is contained in:
+433
-228
@@ -180,6 +180,141 @@
|
||||
max-width: 14rem;
|
||||
}
|
||||
|
||||
.announcement-icon-picker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-height: calc(1.5em + 0.75rem + 2px);
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__toggle:focus-visible {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
|
||||
}
|
||||
|
||||
.announcement-icon-picker__toggle-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(var(--bs-primary-rgb), 0.1);
|
||||
color: var(--bs-primary);
|
||||
font-size: 1rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__toggle-label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__caret {
|
||||
flex: 0 0 auto;
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__menu {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: calc(100% + 0.5rem);
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
width: min(100%, 28rem);
|
||||
max-width: calc(100vw - 1rem);
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 1rem;
|
||||
background: var(--bs-body-bg);
|
||||
box-shadow: 0 1rem 2rem rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
|
||||
.announcement-icon-picker__menu.is-open-above {
|
||||
top: auto;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
}
|
||||
|
||||
.announcement-icon-picker__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(2.75rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0.75rem;
|
||||
background: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
transition: transform 120ms ease, border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__option:hover,
|
||||
.announcement-icon-picker__option:focus-visible {
|
||||
border-color: rgba(var(--bs-primary-rgb), 0.7);
|
||||
background: rgba(var(--bs-primary-rgb), 0.08);
|
||||
box-shadow: 0 0 0 0.15rem rgba(var(--bs-primary-rgb), 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.announcement-icon-picker__option.is-selected {
|
||||
border-color: var(--bs-primary);
|
||||
background: rgba(var(--bs-primary-rgb), 0.12);
|
||||
color: var(--bs-primary);
|
||||
box-shadow: 0 0 0 0.15rem rgba(var(--bs-primary-rgb), 0.12);
|
||||
}
|
||||
|
||||
.announcement-icon-picker__option i {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.announcement-icon-picker__menu {
|
||||
width: calc(100vw - 2rem);
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.announcement-icon-picker__grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(2.5rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.fonts-table-responsive {
|
||||
border-bottom-left-radius: var(--bs-card-border-radius);
|
||||
border-bottom-right-radius: var(--bs-card-border-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.background-tasks-task-tools {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
@@ -194,6 +329,69 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.background-tasks-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-pagination-page .app-main {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.table-pagination-page .app-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-pagination-page .app-content .container-fluid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.background-tasks-page .app-content .container-fluid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.background-tasks-page .app-main {
|
||||
height: 100%;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.background-tasks-page .app-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-table-pagination-card] {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
max-height: var(--table-pagination-card-max-height, var(--background-tasks-task-card-max-height, calc(100dvh - 12rem)));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-table-pagination-card] > .card-body.table-responsive {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[data-table-pagination-card] > .card-footer {
|
||||
margin-top: auto;
|
||||
background: var(--bs-body-bg);
|
||||
border-top: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.admin-form-card {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -202,6 +400,20 @@
|
||||
background: var(--bs-tertiary-bg);
|
||||
}
|
||||
|
||||
.api-source-section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 1rem;
|
||||
padding-bottom: 0.35rem;
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
color: var(--bs-primary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dashboard-hero {
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, rgba(13, 110, 253, 0.08), rgba(32, 201, 151, 0.08));
|
||||
@@ -468,6 +680,11 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chip[data-time-date-placeholder-chip] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--bs-secondary-color);
|
||||
font-style: italic;
|
||||
@@ -532,281 +749,179 @@ td[data-label="Slides"] {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-editor,
|
||||
.template-field-card .ck.ck-editor {
|
||||
--ck-custom-background: var(--bs-body-bg);
|
||||
--ck-custom-foreground: var(--bs-body-color);
|
||||
--ck-custom-border: var(--bs-border-color);
|
||||
--ck-color-base-foreground: var(--bs-tertiary-bg);
|
||||
--ck-color-base-background: var(--bs-body-bg);
|
||||
--ck-color-base-border: var(--bs-border-color);
|
||||
--ck-color-base-text: var(--bs-body-color);
|
||||
--ck-color-base-action: var(--bs-primary);
|
||||
--ck-color-base-focus: var(--bs-primary);
|
||||
--ck-color-base-active: var(--bs-primary);
|
||||
--ck-color-base-active-focus: var(--bs-primary);
|
||||
--ck-color-base-error: var(--bs-danger);
|
||||
--ck-color-focus-border: rgba(var(--bs-primary-rgb), 1);
|
||||
--ck-color-focus-outer-shadow: rgba(var(--bs-primary-rgb), 0.18);
|
||||
--ck-color-focus-disabled-shadow: rgba(var(--bs-secondary-rgb), 0.14);
|
||||
--ck-color-focus-error-shadow: rgba(var(--bs-danger-rgb), 0.18);
|
||||
--ck-color-shadow-drop: rgba(0, 0, 0, 0.16);
|
||||
--ck-color-shadow-drop-active: rgba(0, 0, 0, 0.22);
|
||||
--ck-color-shadow-inner: rgba(0, 0, 0, 0.08);
|
||||
--ck-color-button-default-background: transparent;
|
||||
--ck-color-button-default-hover-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-default-active-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-default-disabled-background: transparent;
|
||||
--ck-color-button-on-background: rgba(var(--bs-primary-rgb), 0.12);
|
||||
--ck-color-button-on-hover-background: rgba(var(--bs-primary-rgb), 0.16);
|
||||
--ck-color-button-on-active-background: rgba(var(--bs-primary-rgb), 0.16);
|
||||
--ck-color-button-on-disabled-background: var(--bs-secondary-bg);
|
||||
--ck-color-button-on-color: var(--bs-primary);
|
||||
--ck-color-button-action-background: var(--bs-primary);
|
||||
--ck-color-button-action-hover-background: var(--bs-primary);
|
||||
--ck-color-button-action-active-background: var(--bs-primary);
|
||||
--ck-color-button-action-disabled-background: var(--bs-primary);
|
||||
--ck-color-button-action-text: var(--bs-body-bg);
|
||||
--ck-color-switch-button-off-background: var(--bs-secondary-color);
|
||||
--ck-color-switch-button-off-hover-background: var(--bs-secondary-color);
|
||||
--ck-color-switch-button-on-background: var(--bs-primary);
|
||||
--ck-color-switch-button-on-hover-background: var(--bs-primary);
|
||||
--ck-color-switch-button-inner-background: var(--bs-body-bg);
|
||||
--ck-color-switch-button-inner-shadow: rgba(0, 0, 0, 0.08);
|
||||
--ck-color-dropdown-panel-background: var(--bs-body-bg);
|
||||
--ck-color-dropdown-panel-border: var(--bs-border-color);
|
||||
--ck-color-dialog-background: var(--bs-body-bg);
|
||||
--ck-color-dialog-form-header-border: var(--bs-border-color);
|
||||
--ck-color-list-button-on-background: rgba(var(--bs-primary-rgb), 0.12);
|
||||
--ck-color-list-button-on-text: var(--bs-primary);
|
||||
--ck-color-input-background: var(--bs-body-bg);
|
||||
--ck-color-input-border: var(--bs-border-color);
|
||||
--ck-color-input-text: var(--bs-body-color);
|
||||
--ck-color-input-disabled-background: var(--bs-secondary-bg);
|
||||
--ck-color-input-disabled-border: var(--bs-border-color);
|
||||
--ck-color-input-disabled-text: var(--bs-secondary-color);
|
||||
--ck-color-toolbar-background: var(--bs-tertiary-bg);
|
||||
--ck-color-toolbar-border: var(--bs-border-color);
|
||||
--ck-color-toolbar-box-shadow: none;
|
||||
--ck-color-image-caption-background: var(--bs-secondary-bg);
|
||||
--ck-color-image-caption-text: var(--bs-secondary-color);
|
||||
--ck-color-resizer: var(--bs-primary);
|
||||
--ck-color-resizer-tooltip-background: var(--bs-dark);
|
||||
--ck-color-resizer-tooltip-text: var(--bs-light);
|
||||
border-radius: var(--bs-border-radius);
|
||||
overflow: hidden;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
.slide-form-stack .editor-holder,
|
||||
.template-field-card .editor-holder {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-editor__top .ck-sticky-panel__content,
|
||||
.template-field-card .ck.ck-editor__top .ck-sticky-panel__content {
|
||||
.slide-form-stack .editor-holder textarea,
|
||||
.template-field-card .editor-holder textarea {
|
||||
width: 100%;
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.slide-form-stack .tox,
|
||||
.template-field-card .tox {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
overflow: visible !important;
|
||||
background: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.slide-form-stack .tox.tox-tinymce,
|
||||
.template-field-card .tox.tox-tinymce {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.slide-form-stack .tox.tox-edit-focus,
|
||||
.template-field-card .tox.tox-edit-focus {
|
||||
border-color: rgba(var(--bs-primary-rgb), 0.22) !important;
|
||||
box-shadow: 0 0 0 0.05rem rgba(var(--bs-primary-rgb), 0.05) !important;
|
||||
}
|
||||
|
||||
.slide-form-stack .tox .tox-edit-area::before,
|
||||
.template-field-card .tox .tox-edit-area::before {
|
||||
border: 1px solid rgba(var(--bs-primary-rgb), 0.08) !important;
|
||||
}
|
||||
|
||||
.slide-form-stack .tox.tox-edit-focus .tox-edit-area::before,
|
||||
.template-field-card .tox.tox-edit-focus .tox-edit-area::before {
|
||||
opacity: 0.35 !important;
|
||||
}
|
||||
|
||||
.slide-form-stack .tox .tox-editor-header,
|
||||
.template-field-card .tox .tox-editor-header {
|
||||
border-bottom: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-toolbar,
|
||||
.template-field-card .ck.ck-toolbar {
|
||||
.slide-form-stack .tox .tox-toolbar,
|
||||
.template-field-card .tox .tox-toolbar {
|
||||
background: var(--bs-tertiary-bg);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-toolbar .ck-toolbar__items,
|
||||
.template-field-card .ck.ck-toolbar .ck-toolbar__items {
|
||||
gap: 0.15rem;
|
||||
.slide-form-stack .tox .tox-toolbar__group,
|
||||
.template-field-card .tox .tox-toolbar__group {
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-toolbar .ck-button,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-dropdown__button,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-splitbutton__action,
|
||||
.template-field-card .ck.ck-toolbar .ck-button,
|
||||
.template-field-card .ck.ck-toolbar .ck-dropdown__button,
|
||||
.template-field-card .ck.ck-toolbar .ck-splitbutton__action {
|
||||
border-radius: 0.5rem;
|
||||
.slide-form-stack .tox .tox-tbtn,
|
||||
.template-field-card .tox .tox-tbtn {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-toolbar .ck-button:hover,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-dropdown__button:hover,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-splitbutton__action:hover,
|
||||
.template-field-card .ck.ck-toolbar .ck-button:hover,
|
||||
.template-field-card .ck.ck-toolbar .ck-dropdown__button:hover,
|
||||
.template-field-card .ck.ck-toolbar .ck-splitbutton__action:hover {
|
||||
.slide-form-stack .tox .tox-tbtn:hover,
|
||||
.template-field-card .tox .tox-tbtn:hover {
|
||||
background: var(--bs-secondary-bg);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-toolbar .ck-button.ck-on,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-dropdown__button.ck-on,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-splitbutton__action.ck-on,
|
||||
.template-field-card .ck.ck-toolbar .ck-button.ck-on,
|
||||
.template-field-card .ck.ck-toolbar .ck-dropdown__button.ck-on,
|
||||
.template-field-card .ck.ck-toolbar .ck-splitbutton__action.ck-on {
|
||||
.slide-form-stack .tox .tox-tbtn--enabled,
|
||||
.template-field-card .tox .tox-tbtn--enabled {
|
||||
background: rgba(var(--bs-primary-rgb), 0.12);
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-toolbar .ck-button.ck-disabled,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-dropdown__button.ck-disabled,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-splitbutton__action.ck-disabled,
|
||||
.template-field-card .ck.ck-toolbar .ck-button.ck-disabled,
|
||||
.template-field-card .ck.ck-toolbar .ck-dropdown__button.ck-disabled,
|
||||
.template-field-card .ck.ck-toolbar .ck-splitbutton__action.ck-disabled {
|
||||
.slide-form-stack .tox .tox-tbtn--disabled,
|
||||
.template-field-card .tox .tox-tbtn--disabled {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-toolbar .ck-font-size-input,
|
||||
.template-field-card .ck.ck-toolbar .ck-font-size-input {
|
||||
flex: 0 0 5% !important;
|
||||
width: 5% !important;
|
||||
max-width: 5% !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-dropdown__panel,
|
||||
.slide-form-stack .ck.ck-list__panel,
|
||||
.slide-form-stack .ck.ck-balloon-panel,
|
||||
.template-field-card .ck.ck-dropdown__panel,
|
||||
.template-field-card .ck.ck-list__panel,
|
||||
.template-field-card .ck.ck-balloon-panel {
|
||||
border-color: var(--bs-border-color);
|
||||
box-shadow: var(--bs-box-shadow);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-list__item .ck-button,
|
||||
.template-field-card .ck.ck-list__item .ck-button {
|
||||
.slide-form-stack .tox .tox-edit-area__iframe,
|
||||
.template-field-card .tox .tox-edit-area__iframe {
|
||||
background: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-list__item .ck-button:hover,
|
||||
.template-field-card .ck.ck-list__item .ck-button:hover {
|
||||
.slide-form-stack .tox .tox-edit-area,
|
||||
.template-field-card .tox .tox-edit-area {
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.slide-form-stack .tox .tox-edit-area__iframe,
|
||||
.template-field-card .tox .tox-edit-area__iframe {
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.slide-form-stack .tox .tox-statusbar,
|
||||
.template-field-card .tox .tox-statusbar {
|
||||
border-top: 1px solid var(--bs-border-color);
|
||||
background: var(--bs-tertiary-bg);
|
||||
}
|
||||
|
||||
.slide-form-stack .tox .tox-menu,
|
||||
.template-field-card .tox .tox-menu,
|
||||
.slide-form-stack .tox .tox-collection,
|
||||
.template-field-card .tox .tox-collection {
|
||||
background: var(--bs-body-bg);
|
||||
border-color: var(--bs-border-color);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .tox .tox-collection__item,
|
||||
.template-field-card .tox .tox-collection__item {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .tox .tox-collection__item:hover,
|
||||
.template-field-card .tox .tox-collection__item:hover {
|
||||
background: var(--bs-secondary-bg);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-dropdown__panel,
|
||||
.slide-form-stack .ck.ck-list__panel,
|
||||
.slide-form-stack .ck.ck-balloon-panel,
|
||||
.slide-form-stack .ck.ck-toolbar .ck-dropdown__panel,
|
||||
.template-field-card .ck.ck-dropdown__panel,
|
||||
.template-field-card .ck.ck-list__panel,
|
||||
.template-field-card .ck.ck-balloon-panel,
|
||||
.template-field-card .ck.ck-toolbar .ck-dropdown__panel {
|
||||
.slide-form-stack .tox .tox-input,
|
||||
.template-field-card .tox .tox-input {
|
||||
background: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-input-text,
|
||||
.slide-form-stack .ck.ck-list__item-text,
|
||||
.slide-form-stack .ck.ck-list__item-label,
|
||||
.slide-form-stack .ck.ck-dropdown__panel .ck-button__label,
|
||||
.template-field-card .ck.ck-input-text,
|
||||
.template-field-card .ck.ck-list__item-text,
|
||||
.template-field-card .ck.ck-list__item-label,
|
||||
.template-field-card .ck.ck-dropdown__panel .ck-button__label {
|
||||
border-color: var(--bs-border-color);
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-input-text,
|
||||
.template-field-card .ck.ck-input-text {
|
||||
background: var(--bs-body-bg);
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-input-text:focus,
|
||||
.template-field-card .ck.ck-input-text:focus {
|
||||
.slide-form-stack .tox .tox-input:focus,
|
||||
.template-field-card .tox .tox-input:focus {
|
||||
border-color: var(--bs-primary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.18);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-editor__main > .ck-editor__editable,
|
||||
.slide-form-stack .ck.ck-editor__editable_inline,
|
||||
.template-field-card .ck.ck-editor__main > .ck-editor__editable,
|
||||
.template-field-card .ck.ck-editor__editable_inline {
|
||||
background: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
border: 0;
|
||||
.slide-form-stack .tox .tox-edit-area__iframe,
|
||||
.template-field-card .tox .tox-edit-area__iframe {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.slide-form-stack .ck.ck-editor__main > .ck-editor__editable.ck-focused,
|
||||
.slide-form-stack .ck.ck-editor__editable_inline.ck-focused,
|
||||
.template-field-card .ck.ck-editor__main > .ck-editor__editable.ck-focused,
|
||||
.template-field-card .ck.ck-editor__editable_inline.ck-focused {
|
||||
box-shadow: inset 0 0 0 1px var(--bs-primary), 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.18);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck-editor__editable_inline,
|
||||
.template-field-card .ck-editor__editable_inline {
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.slide-form-stack .ck-content,
|
||||
.template-field-card .ck-content {
|
||||
.slide-form-stack .tox-content,
|
||||
.template-field-card .tox-content {
|
||||
font-family: inherit;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.slide-form-stack .ck-content h1,
|
||||
.slide-form-stack .ck-content h2,
|
||||
.slide-form-stack .ck-content h3,
|
||||
.slide-form-stack .ck-content h4,
|
||||
.slide-form-stack .ck-content h5,
|
||||
.slide-form-stack .ck-content h6,
|
||||
.template-field-card .ck-content h1,
|
||||
.template-field-card .ck-content h2,
|
||||
.template-field-card .ck-content h3,
|
||||
.template-field-card .ck-content h4,
|
||||
.template-field-card .ck-content h5,
|
||||
.template-field-card .ck-content h6 {
|
||||
.slide-form-stack .tox-content h1,
|
||||
.slide-form-stack .tox-content h2,
|
||||
.slide-form-stack .tox-content h3,
|
||||
.slide-form-stack .tox-content h4,
|
||||
.slide-form-stack .tox-content h5,
|
||||
.slide-form-stack .tox-content h6,
|
||||
.template-field-card .tox-content h1,
|
||||
.template-field-card .tox-content h2,
|
||||
.template-field-card .tox-content h3,
|
||||
.template-field-card .tox-content h4,
|
||||
.template-field-card .tox-content h5,
|
||||
.template-field-card .tox-content h6 {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.slide-form-stack .ck-content p,
|
||||
.template-field-card .ck-content p {
|
||||
.slide-form-stack .tox-content p,
|
||||
.template-field-card .tox-content p {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-dropdown__panel,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list__panel,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-balloon-panel {
|
||||
html[data-bs-theme='dark'] .slide-form-stack .tox,
|
||||
html[data-bs-theme='dark'] .template-field-card .tox,
|
||||
html[data-bs-theme='dark'] .slide-form-stack .tox .tox-menu,
|
||||
html[data-bs-theme='dark'] .template-field-card .tox .tox-menu,
|
||||
html[data-bs-theme='dark'] .slide-form-stack .tox .tox-collection,
|
||||
html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
background: var(--bs-body-bg) !important;
|
||||
background-color: var(--bs-body-bg) !important;
|
||||
border-color: var(--bs-border-color) !important;
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list__item .ck-button,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list .ck-button,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list__item .ck-button__label,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list .ck-button__label,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-dropdown__panel .ck-button__label,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-dropdown__panel .ck-list__item-text,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-dropdown__panel .ck-list__item-label,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid,
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list .ck-button {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list .ck-list-item-button {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list .ck-list-item-button:hover {
|
||||
background: var(--bs-secondary-bg) !important;
|
||||
background-color: var(--bs-secondary-bg) !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-list .ck-button:hover {
|
||||
background: var(--bs-secondary-bg) !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile {
|
||||
border-color: var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
|
||||
.slide-preview-dimensions-chip {
|
||||
display: inline-flex;
|
||||
@@ -1137,7 +1252,7 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile {
|
||||
|
||||
.template-designer-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 20rem;
|
||||
grid-template-columns: minmax(0, 1fr) 25rem;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
@@ -1435,7 +1550,8 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile {
|
||||
}
|
||||
|
||||
.template-field-card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.template-field-card .card-header,
|
||||
@@ -1444,11 +1560,6 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile {
|
||||
border-bottom: 1px solid var(--bs-border-color-translucent);
|
||||
}
|
||||
|
||||
.template-field-card .card-body,
|
||||
.region-item .card-body {
|
||||
background: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
.template-field-card label,
|
||||
.region-item label {
|
||||
margin-bottom: 0;
|
||||
@@ -1465,11 +1576,105 @@ html[data-bs-theme='dark'] .ck-body-wrapper .ck.ck-color-grid__tile {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-field-card .ckeditor-holder {
|
||||
.template-field-card .editor-holder {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.template-field-card.is-expanded .ck-editor__editable_inline {
|
||||
.announcement-color-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 5.5rem;
|
||||
height: calc(2.375rem + 2px);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
}
|
||||
|
||||
.announcement-icon-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 5.5rem;
|
||||
height: calc(2.375rem + 2px);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
background: var(--bs-tertiary-bg);
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.announcement-list-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 999px;
|
||||
flex: 0 0 auto;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.api-region-placeholder-section {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.api-region-placeholder-title {
|
||||
color: var(--bs-primary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.api-region-sample-accordion {
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0.85rem;
|
||||
background: var(--bs-secondary-bg);
|
||||
}
|
||||
|
||||
.api-region-sample-accordion > summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
color: var(--bs-primary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.api-region-sample-accordion > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.api-region-sample-body {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.api-region-sample-meta {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.api-region-sample-preview {
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.65rem;
|
||||
background: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color-translucent);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.template-field-card.is-expanded .tox-edit-area__iframe {
|
||||
min-height: 32rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,37 @@
|
||||
}
|
||||
|
||||
function initAsyncCommandForms() {
|
||||
function getAnnouncementActionState(actionPath) {
|
||||
var isPlay = /\/play$/.test(actionPath);
|
||||
return {
|
||||
nextActionPath: actionPath.replace(/\/(?:play|stop)$/, isPlay ? '/stop' : '/play'),
|
||||
nextLabel: isPlay ? 'Stop' : 'Play',
|
||||
nextIcon: isPlay ? 'bi-stop-fill' : 'bi-play-fill',
|
||||
nextClassName: isPlay ? 'btn-outline-warning' : 'btn-outline-success',
|
||||
nextConfirmMessage: isPlay
|
||||
? 'Stop this announcement on the selected screens now?'
|
||||
: 'Send this announcement to the selected screens now?'
|
||||
};
|
||||
}
|
||||
|
||||
function updateAnnouncementActionButton(form, actionPath) {
|
||||
if (!form) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var state = getAnnouncementActionState(actionPath);
|
||||
var visibleButton = form.querySelector('button[type="submit"]') || (form.id ? document.querySelector('button[form="' + form.id + '"]') : null);
|
||||
if (!visibleButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
form.action = state.nextActionPath;
|
||||
form.setAttribute('data-confirm-message', state.nextConfirmMessage);
|
||||
visibleButton.className = visibleButton.className.replace(/btn-outline-(success|warning)/g, state.nextClassName);
|
||||
visibleButton.innerHTML = '<i class="bi ' + state.nextIcon + ' me-1" aria-hidden="true"></i>' + state.nextLabel;
|
||||
return true;
|
||||
}
|
||||
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-command')) {
|
||||
@@ -97,6 +128,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var actionPath = '';
|
||||
try {
|
||||
actionPath = new URL(form.action, window.location.href).pathname;
|
||||
} catch (_error) {
|
||||
actionPath = String(form.action || '');
|
||||
}
|
||||
var shouldReloadAfterSuccess = /^\/announcements\/\d+\/(?:play|stop)$/.test(actionPath);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
form.dataset.busy = 'true';
|
||||
@@ -116,6 +155,11 @@
|
||||
},
|
||||
body: body.toString(),
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (shouldReloadAfterSuccess && response && response.ok) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
}).finally(function () {
|
||||
delete form.dataset.busy;
|
||||
});
|
||||
@@ -426,6 +470,52 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderJsonPrimitive(value) {
|
||||
if (value === null) {
|
||||
return '<span class="text-body-secondary">null</span>';
|
||||
}
|
||||
if (value === true || value === false) {
|
||||
return '<span class="text-success">' + String(value) + '</span>';
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return '<span class="text-info">' + escapeHtml(String(value)) + '</span>';
|
||||
}
|
||||
return '<span class="text-body">"' + escapeHtml(String(value)) + '"</span>';
|
||||
}
|
||||
|
||||
function renderJsonNode(key, value, isRoot) {
|
||||
var isArray = Array.isArray(value);
|
||||
var isObject = value && typeof value === 'object' && !isArray;
|
||||
|
||||
if (!isArray && !isObject) {
|
||||
var primitiveLabel = isRoot ? 'value' : String(key);
|
||||
return '<div class="json-tree-leaf"><span class="json-tree-key">' + escapeHtml(primitiveLabel) + '</span><span class="mx-1">:</span>' + renderJsonPrimitive(value) + '</div>';
|
||||
}
|
||||
|
||||
var entries = isArray
|
||||
? value.map(function (entry, index) {
|
||||
return renderJsonNode('[' + index + ']', entry, false);
|
||||
}).join('')
|
||||
: Object.keys(value).map(function (childKey) {
|
||||
return renderJsonNode(childKey, value[childKey], false);
|
||||
}).join('');
|
||||
|
||||
var summaryLabel = isRoot ? (isArray ? 'Array' : 'Object') : String(key);
|
||||
var summaryMeta = isArray ? '[' + value.length + ']' : '{' + Object.keys(value).length + '}';
|
||||
return '' +
|
||||
'<details class="json-tree-node" open>' +
|
||||
'<summary><span class="json-tree-key">' + escapeHtml(summaryLabel) + '</span><span class="mx-1">:</span><span class="text-body-secondary">' + escapeHtml(summaryMeta) + '</span></summary>' +
|
||||
'<div class="ms-3 ps-3 border-start">' + entries + '</div>' +
|
||||
'</details>';
|
||||
}
|
||||
|
||||
function renderJsonTree(value) {
|
||||
if (Array.isArray(value) || (value && typeof value === 'object')) {
|
||||
return renderJsonNode('', value, true);
|
||||
}
|
||||
return renderJsonNode('value', value, true);
|
||||
}
|
||||
|
||||
function initJsonTogglePanels(root) {
|
||||
var scope = root && root.querySelectorAll ? root : document;
|
||||
var panels = scope.querySelectorAll('[data-json-toggle-panel]');
|
||||
@@ -436,8 +526,8 @@
|
||||
}
|
||||
|
||||
var card = panel.closest ? panel.closest('.card') : null;
|
||||
var button = card ? card.querySelector('[data-json-toggle]') : null;
|
||||
var label = button ? button.querySelector('[data-json-toggle-label]') : null;
|
||||
var collapseButton = card ? card.querySelector('[data-json-toggle-collapse-all]') : null;
|
||||
var expandButton = card ? card.querySelector('[data-json-toggle-expand-all]') : null;
|
||||
var sourceNode = panel.querySelector('[data-json-toggle-source]');
|
||||
var rawJson = '';
|
||||
try {
|
||||
@@ -463,32 +553,47 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var compactJson = JSON.stringify(parsedJson);
|
||||
var formattedJson = JSON.stringify(parsedJson, null, 2);
|
||||
var isFormatted = true;
|
||||
var formattedTree = renderJsonTree(parsedJson);
|
||||
|
||||
function syncButtonLabel() {
|
||||
if (!button || !label) {
|
||||
return;
|
||||
function setAllSectionsExpanded(expanded) {
|
||||
var details = output.querySelectorAll('details');
|
||||
Array.prototype.forEach.call(details, function (detail) {
|
||||
detail.open = expanded;
|
||||
});
|
||||
|
||||
if (collapseButton) {
|
||||
collapseButton.disabled = !details.length || !expanded;
|
||||
}
|
||||
if (expandButton) {
|
||||
expandButton.disabled = !details.length || expanded;
|
||||
}
|
||||
label.textContent = isFormatted
|
||||
? String(button.getAttribute('data-json-toggle-label-compact') || 'Unformat JSON')
|
||||
: String(button.getAttribute('data-json-toggle-label-formatted') || 'Format JSON');
|
||||
button.setAttribute('aria-pressed', isFormatted ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function syncOutput() {
|
||||
output.textContent = isFormatted ? formattedJson : compactJson;
|
||||
syncButtonLabel();
|
||||
function syncButtons() {
|
||||
if (collapseButton) {
|
||||
collapseButton.setAttribute('aria-pressed', 'false');
|
||||
}
|
||||
if (expandButton) {
|
||||
expandButton.setAttribute('aria-pressed', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
output.textContent = formattedJson;
|
||||
syncButtonLabel();
|
||||
output.innerHTML = formattedTree;
|
||||
syncButtons();
|
||||
setAllSectionsExpanded(true);
|
||||
|
||||
if (button) {
|
||||
button.addEventListener('click', function () {
|
||||
isFormatted = !isFormatted;
|
||||
syncOutput();
|
||||
if (collapseButton) {
|
||||
collapseButton.addEventListener('click', function () {
|
||||
setAllSectionsExpanded(false);
|
||||
syncButtons();
|
||||
});
|
||||
}
|
||||
|
||||
if (expandButton) {
|
||||
expandButton.addEventListener('click', function () {
|
||||
setAllSectionsExpanded(true);
|
||||
syncButtons();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
function updateAnnouncementDurationFields() {
|
||||
var modeSelect = document.getElementById('announcement-duration-mode');
|
||||
var durationFields = document.querySelector('[data-announcement-duration-value-fields]');
|
||||
if (!modeSelect || !durationFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
var durationValueInput = document.getElementById('announcement-duration-value');
|
||||
var durationUnitSelect = document.getElementById('announcement-duration-unit');
|
||||
var isDurationMode = String(modeSelect.value || '').trim() === 'duration';
|
||||
|
||||
durationFields.hidden = !isDurationMode;
|
||||
if (durationValueInput) {
|
||||
durationValueInput.disabled = !isDurationMode;
|
||||
}
|
||||
if (durationUnitSelect) {
|
||||
durationUnitSelect.disabled = !isDurationMode;
|
||||
}
|
||||
}
|
||||
|
||||
function updateAnnouncementColorPreview() {
|
||||
var colorSelect = document.getElementById('announcement-color');
|
||||
var preview = document.querySelector('[data-announcement-color-preview]');
|
||||
if (!colorSelect || !preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedOption = colorSelect.options[colorSelect.selectedIndex] || null;
|
||||
var colorKey = String(colorSelect.value || '').trim().toLowerCase() || 'primary';
|
||||
var label = selectedOption ? String(selectedOption.textContent || selectedOption.label || colorKey).trim() : colorKey;
|
||||
|
||||
preview.className = 'announcement-color-preview text-bg-' + colorKey;
|
||||
preview.setAttribute('aria-label', label);
|
||||
preview.setAttribute('title', label);
|
||||
preview.textContent = '';
|
||||
}
|
||||
|
||||
function updateAnnouncementIconPreview() {
|
||||
var iconSelect = document.getElementById('announcement-icon');
|
||||
var preview = document.querySelector('[data-announcement-icon-preview]');
|
||||
if (!iconSelect || !preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedOption = iconSelect.options[iconSelect.selectedIndex] || null;
|
||||
var iconKey = selectedOption ? String(selectedOption.value || '').trim().toLowerCase() : '';
|
||||
var label = selectedOption ? String(selectedOption.textContent || selectedOption.label || iconKey).trim() : iconKey;
|
||||
|
||||
preview.className = 'announcement-icon-preview';
|
||||
preview.innerHTML = '<i class="bi bi-' + iconKey + '" aria-hidden="true"></i>';
|
||||
preview.setAttribute('aria-label', label);
|
||||
preview.setAttribute('title', label);
|
||||
}
|
||||
|
||||
function positionAnnouncementIconPicker() {
|
||||
var picker = document.querySelector('[data-announcement-icon-picker]');
|
||||
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
|
||||
var shell = document.querySelector('[data-announcement-icon-picker-shell]');
|
||||
if (!picker || picker.hidden || !toggle || !shell) {
|
||||
return;
|
||||
}
|
||||
|
||||
var padding = 8;
|
||||
var toggleRect = toggle.getBoundingClientRect();
|
||||
var menuRect = picker.getBoundingClientRect();
|
||||
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || toggleRect.bottom;
|
||||
var placementAbove = false;
|
||||
var spaceBelow = viewportHeight - toggleRect.bottom - padding;
|
||||
var spaceAbove = toggleRect.top - padding;
|
||||
|
||||
if (menuRect.height > spaceBelow && spaceAbove > spaceBelow) {
|
||||
placementAbove = true;
|
||||
}
|
||||
|
||||
picker.classList.toggle('is-open-above', placementAbove);
|
||||
}
|
||||
|
||||
function closeAnnouncementIconPicker() {
|
||||
var picker = document.querySelector('[data-announcement-icon-picker]');
|
||||
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
|
||||
if (!picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
picker.hidden = true;
|
||||
picker.classList.remove('is-open-above');
|
||||
if (toggle) {
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function openAnnouncementIconPicker() {
|
||||
var picker = document.querySelector('[data-announcement-icon-picker]');
|
||||
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
|
||||
if (!picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
picker.hidden = false;
|
||||
if (toggle) {
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame(positionAnnouncementIconPicker);
|
||||
} else {
|
||||
positionAnnouncementIconPicker();
|
||||
}
|
||||
}
|
||||
|
||||
function updateAnnouncementIconPickerSelection() {
|
||||
var iconSelect = document.getElementById('announcement-icon');
|
||||
var previewButton = document.querySelector('[data-announcement-icon-picker-toggle]');
|
||||
var picker = document.querySelector('[data-announcement-icon-picker]');
|
||||
if (!iconSelect || !previewButton || !picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedOption = iconSelect.options[iconSelect.selectedIndex] || null;
|
||||
var selectedValue = selectedOption ? String(selectedOption.value || '').trim().toLowerCase() : '';
|
||||
var label = selectedOption ? String(selectedOption.textContent || selectedOption.label || selectedValue).trim() : selectedValue;
|
||||
var icon = previewButton.querySelector('[data-announcement-icon-picker-icon]');
|
||||
var text = previewButton.querySelector('[data-announcement-icon-picker-label]');
|
||||
|
||||
previewButton.setAttribute('aria-label', label);
|
||||
previewButton.setAttribute('title', label);
|
||||
if (icon) {
|
||||
icon.className = 'bi bi-' + selectedValue;
|
||||
}
|
||||
if (text) {
|
||||
text.textContent = label;
|
||||
}
|
||||
|
||||
picker.querySelectorAll('[data-announcement-icon-option]').forEach(function (button) {
|
||||
var isSelected = String(button.getAttribute('data-icon-key') || '').trim().toLowerCase() === selectedValue;
|
||||
button.classList.toggle('is-selected', isSelected);
|
||||
button.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function setAnnouncementIconValue(iconKey) {
|
||||
var iconSelect = document.getElementById('announcement-icon');
|
||||
if (!iconSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
var normalized = String(iconKey || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
iconSelect.value = normalized;
|
||||
updateAnnouncementIconPreview();
|
||||
updateAnnouncementIconPickerSelection();
|
||||
}
|
||||
|
||||
function initAnnouncementForm() {
|
||||
var modeSelect = document.getElementById('announcement-duration-mode');
|
||||
var colorSelect = document.getElementById('announcement-color');
|
||||
var iconSelect = document.getElementById('announcement-icon');
|
||||
var iconPickerToggle = document.querySelector('[data-announcement-icon-picker-toggle]');
|
||||
var iconPicker = document.querySelector('[data-announcement-icon-picker]');
|
||||
var iconPickerClose = document.querySelector('[data-announcement-icon-picker-close]');
|
||||
|
||||
if (modeSelect) {
|
||||
modeSelect.addEventListener('change', updateAnnouncementDurationFields);
|
||||
updateAnnouncementDurationFields();
|
||||
}
|
||||
|
||||
if (colorSelect) {
|
||||
colorSelect.addEventListener('change', updateAnnouncementColorPreview);
|
||||
updateAnnouncementColorPreview();
|
||||
}
|
||||
|
||||
if (iconSelect) {
|
||||
iconSelect.addEventListener('change', updateAnnouncementIconPreview);
|
||||
updateAnnouncementIconPreview();
|
||||
}
|
||||
|
||||
if (iconPickerToggle) {
|
||||
iconPickerToggle.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
var isExpanded = iconPicker && !iconPicker.hidden;
|
||||
if (isExpanded) {
|
||||
closeAnnouncementIconPicker();
|
||||
} else {
|
||||
openAnnouncementIconPicker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (iconPickerClose) {
|
||||
iconPickerClose.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
closeAnnouncementIconPicker();
|
||||
});
|
||||
}
|
||||
|
||||
if (iconPicker) {
|
||||
iconPicker.addEventListener('click', function (event) {
|
||||
var button = event.target && event.target.closest ? event.target.closest('[data-announcement-icon-option]') : null;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
setAnnouncementIconValue(button.getAttribute('data-icon-key'));
|
||||
closeAnnouncementIconPicker();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', positionAnnouncementIconPicker);
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var picker = document.querySelector('[data-announcement-icon-picker]');
|
||||
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
|
||||
if (!picker || picker.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (picker.contains(event.target) || (toggle && toggle.contains(event.target))) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeAnnouncementIconPicker();
|
||||
});
|
||||
|
||||
updateAnnouncementIconPickerSelection();
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initAnnouncementForm, { once: true });
|
||||
} else {
|
||||
initAnnouncementForm();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// API source form visibility helpers.
|
||||
|
||||
(function () {
|
||||
var form = document.getElementById('api-source-form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
var methodSelect = form.querySelector('[data-api-source-auth-method]');
|
||||
var authDetailsSection = form.querySelector('[data-api-source-auth-details-section]');
|
||||
var panels = Array.prototype.slice.call(form.querySelectorAll('[data-api-source-auth-panel]'));
|
||||
|
||||
function updatePanels() {
|
||||
var method = String(methodSelect && methodSelect.value || 'none').trim();
|
||||
var hasAuth = method !== 'none';
|
||||
|
||||
if (authDetailsSection) {
|
||||
authDetailsSection.hidden = !hasAuth;
|
||||
}
|
||||
|
||||
panels.forEach(function (panel) {
|
||||
var panelMethod = String(panel.getAttribute('data-api-source-auth-panel') || '').trim();
|
||||
panel.hidden = !hasAuth || panelMethod !== method;
|
||||
});
|
||||
}
|
||||
|
||||
if (methodSelect) {
|
||||
methodSelect.addEventListener('change', updatePanels);
|
||||
}
|
||||
|
||||
updatePanels();
|
||||
}());
|
||||
@@ -1,3 +1,5 @@
|
||||
// Bootstrap modal convenience wrapper for the browser scripts.
|
||||
|
||||
(function () {
|
||||
function getOrCreate(modalElement) {
|
||||
if (!modalElement || !window.bootstrap || !window.bootstrap.Modal) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// RBAC permission matrix helpers for checkbox groups.
|
||||
|
||||
(function () {
|
||||
function getGroupCheckboxes(group) {
|
||||
return Array.prototype.slice.call(group.querySelectorAll('input[name="permission_keys[]"]'));
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Shared registry for region-type helpers used by the slide and template editors.
|
||||
|
||||
(function () {
|
||||
var root = window;
|
||||
var registry = root.pulseRegionTypes && typeof root.pulseRegionTypes === 'object' ? root.pulseRegionTypes : {};
|
||||
|
||||
function normalizeType(type) {
|
||||
return String(type || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function parseLockRatio(value) {
|
||||
var raw = String(value || '').trim();
|
||||
if (!/^[0-9]+\s*:[0-9]+$/.test(raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = raw.replace(/\s+/g, '').split(':');
|
||||
var width = Number(parts[0]);
|
||||
var height = Number(parts[1]);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { label: parts[0] + ':' + parts[1], ratio: width / height };
|
||||
}
|
||||
|
||||
function getFallbackDefaultSize(regionType, lockRatio) {
|
||||
var ratio = parseLockRatio(lockRatio);
|
||||
|
||||
if (ratio) {
|
||||
if (ratio.ratio >= 1) {
|
||||
var baseWidth = 300;
|
||||
return {
|
||||
width: Math.max(12, Math.round(baseWidth)),
|
||||
height: Math.max(12, Math.round(baseWidth / ratio.ratio))
|
||||
};
|
||||
}
|
||||
|
||||
var baseHeight = 240;
|
||||
return {
|
||||
width: Math.max(12, Math.round(baseHeight * ratio.ratio)),
|
||||
height: Math.max(12, Math.round(baseHeight))
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: 300,
|
||||
height: 120
|
||||
};
|
||||
}
|
||||
|
||||
function register(type, definition) {
|
||||
registry[normalizeType(type)] = definition || {};
|
||||
return registry[normalizeType(type)];
|
||||
}
|
||||
|
||||
function get(type) {
|
||||
return registry[normalizeType(type)] || null;
|
||||
}
|
||||
|
||||
function list() {
|
||||
return Object.keys(registry).map(function (type) {
|
||||
return {
|
||||
type: type,
|
||||
definition: registry[type] || {}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultRegionSize(regionType, lockRatio) {
|
||||
var definition = get(regionType);
|
||||
if (definition && typeof definition.getDefaultRegionSize === 'function') {
|
||||
return definition.getDefaultRegionSize(lockRatio);
|
||||
}
|
||||
|
||||
return getFallbackDefaultSize(normalizeType(regionType), lockRatio);
|
||||
}
|
||||
|
||||
function getDefaultRegionStyle(regionType) {
|
||||
var definition = get(regionType);
|
||||
if (definition && typeof definition.getDefaultStyle === 'function') {
|
||||
return definition.getDefaultStyle();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getRegionChipLabel(regionType) {
|
||||
var definition = get(regionType);
|
||||
if (definition && definition.label) {
|
||||
return String(definition.label);
|
||||
}
|
||||
|
||||
var normalized = normalizeType(regionType);
|
||||
if (!normalized) {
|
||||
return 'Region';
|
||||
}
|
||||
|
||||
return normalized
|
||||
.split(/[_-]+/)
|
||||
.filter(function (segment) { return segment; })
|
||||
.map(function (segment) {
|
||||
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
||||
})
|
||||
.join(' ') || 'Region';
|
||||
}
|
||||
|
||||
root.pulseRegionTypes = {
|
||||
register: register,
|
||||
get: get,
|
||||
list: list,
|
||||
getDefaultRegionSize: getDefaultRegionSize,
|
||||
getDefaultRegionStyle: getDefaultRegionStyle,
|
||||
getRegionChipLabel: getRegionChipLabel
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,772 @@
|
||||
// Shared browser utilities for region helper modules.
|
||||
|
||||
(function () {
|
||||
function escapeHtml(value) {
|
||||
if (window.webUiHelpers && typeof window.webUiHelpers.escapeHtml === 'function') {
|
||||
return window.webUiHelpers.escapeHtml(value);
|
||||
}
|
||||
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function sanitizePreviewHtml(html) {
|
||||
var output = String(html || '');
|
||||
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
output = output.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||||
return output.replace(/<[^>]+>/g, function (tag) {
|
||||
var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)([\s\S]*?)(\/?)>$/i);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var closing = Boolean(match[1]);
|
||||
var name = String(match[2] || '').toLowerCase();
|
||||
var attrText = String(match[3] || '');
|
||||
var selfClosing = Boolean(match[4]) || name === 'br' || name === 'hr';
|
||||
var allowed = ['a', 'b', 'blockquote', 'br', 'code', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'];
|
||||
if (allowed.indexOf(name) === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (closing) {
|
||||
return '</' + name + '>';
|
||||
}
|
||||
|
||||
return '<' + name + sanitizeTagAttributes(name, attrText) + '>';
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeRichText(html) {
|
||||
return sanitizePreviewHtml(html);
|
||||
}
|
||||
|
||||
function sanitizeTagAttributes(tagName, attrText) {
|
||||
var allowedAttributes = {
|
||||
a: ['href', 'title', 'target', 'rel', 'class', 'style'],
|
||||
blockquote: ['class', 'style'],
|
||||
div: ['class', 'style'],
|
||||
figure: ['class', 'style'],
|
||||
figcaption: ['class', 'style'],
|
||||
h1: ['class', 'style'],
|
||||
h2: ['class', 'style'],
|
||||
h3: ['class', 'style'],
|
||||
h4: ['class', 'style'],
|
||||
h5: ['class', 'style'],
|
||||
h6: ['class', 'style'],
|
||||
li: ['class', 'style'],
|
||||
ol: ['class', 'style', 'start'],
|
||||
p: ['class', 'style'],
|
||||
pre: ['class', 'style'],
|
||||
span: ['class', 'style'],
|
||||
table: ['class', 'style'],
|
||||
td: ['class', 'style', 'colspan', 'rowspan'],
|
||||
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
|
||||
tr: ['class', 'style'],
|
||||
ul: ['class', 'style']
|
||||
};
|
||||
var allowed = allowedAttributes[tagName] || [];
|
||||
if (!allowed.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var attrs = [];
|
||||
attrText.replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, function (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) {
|
||||
var lowerKey = String(key || '').toLowerCase();
|
||||
if (allowed.indexOf(lowerKey) === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : '';
|
||||
if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) {
|
||||
return '';
|
||||
}
|
||||
if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) {
|
||||
return '';
|
||||
}
|
||||
if (lowerKey === 'target') {
|
||||
var targetValue = String(value || '').trim();
|
||||
if (targetValue === '_blank') {
|
||||
attrs.push(' target="_blank"');
|
||||
if (attrs.indexOf(' rel="noreferrer noopener"') === -1) {
|
||||
attrs.push(' rel="noreferrer noopener"');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"');
|
||||
return '';
|
||||
});
|
||||
|
||||
return attrs.join('');
|
||||
}
|
||||
|
||||
function sanitizeFontFamily(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function sanitizeTextColor(value) {
|
||||
return String(value || '').trim() || '#000000';
|
||||
}
|
||||
|
||||
function normalizeAcceptList(value) {
|
||||
return Array.isArray(value) ? value : String(value || '').split(',').map(function (item) {
|
||||
return String(item || '').trim();
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function fileMatchesAccept(file, acceptValue) {
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var acceptList = normalizeAcceptList(acceptValue).map(function (item) {
|
||||
return item.toLowerCase();
|
||||
});
|
||||
|
||||
if (!acceptList.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var fileName = String(file.name || '').toLowerCase();
|
||||
var fileType = String(file.type || '').toLowerCase();
|
||||
|
||||
return acceptList.some(function (rule) {
|
||||
if (rule === '*/*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rule.charAt(0) === '.') {
|
||||
return fileName.endsWith(rule);
|
||||
}
|
||||
|
||||
if (rule.endsWith('/*')) {
|
||||
return fileType.indexOf(rule.slice(0, -1)) === 0;
|
||||
}
|
||||
|
||||
return fileType === rule;
|
||||
});
|
||||
}
|
||||
|
||||
function bindRegionMediaRemoveControls(templateFields, options) {
|
||||
var callbacks = options || {};
|
||||
|
||||
if (!templateFields || templateFields.dataset.regionMediaRemoveBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
templateFields.dataset.regionMediaRemoveBound = '1';
|
||||
|
||||
function getCard(regionId) {
|
||||
if (typeof callbacks.getCardByRegionId === 'function') {
|
||||
return callbacks.getCardByRegionId(regionId);
|
||||
}
|
||||
|
||||
return regionId ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
}
|
||||
|
||||
function getMediaType(button, card) {
|
||||
if (typeof callbacks.getMediaType === 'function') {
|
||||
return callbacks.getMediaType(card, button);
|
||||
}
|
||||
|
||||
if (button && button.hasAttribute('data-remove-region-video')) {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
if (button && button.hasAttribute('data-remove-region-image')) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (card && card.dataset && String(card.dataset.regionMediaType || '').trim()) {
|
||||
return String(card.dataset.regionMediaType || '').trim() === 'video' ? 'video' : 'image';
|
||||
}
|
||||
|
||||
return 'image';
|
||||
}
|
||||
|
||||
function getExistingPrefix(mediaType) {
|
||||
if (typeof callbacks.getExistingMediaPrefix === 'function') {
|
||||
return callbacks.getExistingMediaPrefix(mediaType);
|
||||
}
|
||||
|
||||
return String(mediaType || '').trim() === 'video' ? 'existing_region_video_' : 'existing_region_image_';
|
||||
}
|
||||
|
||||
function getMediaPrefix(mediaType) {
|
||||
if (typeof callbacks.getMediaPrefix === 'function') {
|
||||
return callbacks.getMediaPrefix(mediaType);
|
||||
}
|
||||
|
||||
return String(mediaType || '').trim() === 'video' ? 'region_video_' : 'region_image_';
|
||||
}
|
||||
|
||||
function getVideoDurationHiddenInput(regionId) {
|
||||
if (typeof callbacks.getVideoDurationHiddenInput === 'function') {
|
||||
return callbacks.getVideoDurationHiddenInput(regionId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleRemove(button) {
|
||||
var regionId = button.getAttribute('data-remove-region-image') || button.getAttribute('data-remove-region-video');
|
||||
var card = getCard(regionId);
|
||||
var mediaType = getMediaType(button, card);
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="' + getExistingPrefix(mediaType) + regionId + '"]');
|
||||
var input = card && card.querySelector('input[type="file"][name="' + getMediaPrefix(mediaType) + regionId + '"]');
|
||||
var uploadPath = input ? String(input.dataset.uploadedPath || '').trim() : '';
|
||||
|
||||
if (input && input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
|
||||
if (input && input.dataset.uploadedNeedsCleanup === '1' && uploadPath && typeof callbacks.queueUploadCleanup === 'function') {
|
||||
callbacks.queueUploadCleanup([uploadPath]);
|
||||
}
|
||||
|
||||
if (input) {
|
||||
input.value = '';
|
||||
delete input.dataset.uploadedPath;
|
||||
delete input.dataset.uploadedNeedsCleanup;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
hidden.value = '';
|
||||
}
|
||||
|
||||
var durationHidden = getVideoDurationHiddenInput(regionId);
|
||||
if (durationHidden) {
|
||||
durationHidden.value = '';
|
||||
}
|
||||
|
||||
if (typeof callbacks.renderPreview === 'function') {
|
||||
callbacks.renderPreview(card, '');
|
||||
}
|
||||
|
||||
if (typeof callbacks.markEdited === 'function') {
|
||||
callbacks.markEdited();
|
||||
}
|
||||
|
||||
if (typeof callbacks.requestPreviewRender === 'function') {
|
||||
callbacks.requestPreviewRender();
|
||||
}
|
||||
}
|
||||
|
||||
templateFields.addEventListener('click', function (event) {
|
||||
var button = event.target && event.target.closest ? event.target.closest('[data-remove-region-image], [data-remove-region-video]') : null;
|
||||
|
||||
if (!button || !templateFields.contains(button)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleRemove(button);
|
||||
});
|
||||
|
||||
templateFields.addEventListener('keydown', function (event) {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||
return;
|
||||
}
|
||||
|
||||
var button = event.target && event.target.closest ? event.target.closest('[data-remove-region-image], [data-remove-region-video]') : null;
|
||||
|
||||
if (!button || !templateFields.contains(button)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
handleRemove(button);
|
||||
});
|
||||
}
|
||||
|
||||
function createRegionMediaUploadController(templateFields, options) {
|
||||
var callbacks = options || {};
|
||||
|
||||
if (!templateFields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCard(regionId) {
|
||||
if (typeof callbacks.getCardByRegionId === 'function') {
|
||||
return callbacks.getCardByRegionId(regionId);
|
||||
}
|
||||
|
||||
return regionId ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
}
|
||||
|
||||
function getMediaTypeFromInput(input) {
|
||||
if (typeof callbacks.getMediaTypeFromInput === 'function') {
|
||||
return callbacks.getMediaTypeFromInput(input);
|
||||
}
|
||||
|
||||
return input && input.name && input.name.indexOf('region_video_') === 0 ? 'video' : 'image';
|
||||
}
|
||||
|
||||
function getMediaPrefix(mediaType) {
|
||||
if (typeof callbacks.getMediaPrefix === 'function') {
|
||||
return callbacks.getMediaPrefix(mediaType);
|
||||
}
|
||||
|
||||
return String(mediaType || '').trim() === 'video' ? 'region_video_' : 'region_image_';
|
||||
}
|
||||
|
||||
function getExistingMediaPrefix(mediaType) {
|
||||
if (typeof callbacks.getExistingMediaPrefix === 'function') {
|
||||
return callbacks.getExistingMediaPrefix(mediaType);
|
||||
}
|
||||
|
||||
return String(mediaType || '').trim() === 'video' ? 'existing_region_video_' : 'existing_region_image_';
|
||||
}
|
||||
|
||||
function getUploadZone(input) {
|
||||
if (typeof callbacks.getUploadZone === 'function') {
|
||||
return callbacks.getUploadZone(input);
|
||||
}
|
||||
|
||||
return input ? input.closest('[data-region-upload-zone]') : null;
|
||||
}
|
||||
|
||||
function getUploadProgressNode(zone) {
|
||||
return zone ? zone.querySelector('[data-region-upload-progress]') : null;
|
||||
}
|
||||
|
||||
function getUploadProgressBar(zone) {
|
||||
return zone ? zone.querySelector('[data-region-upload-progress-bar]') : null;
|
||||
}
|
||||
|
||||
function getUploadProgressText(zone) {
|
||||
return zone ? zone.querySelector('[data-region-upload-progress-text]') : null;
|
||||
}
|
||||
|
||||
function setUploadProgress(zone, percent, text, isIndeterminate) {
|
||||
var progress = getUploadProgressNode(zone);
|
||||
var bar = getUploadProgressBar(zone);
|
||||
var progressText = getUploadProgressText(zone);
|
||||
var normalizedPercent = Math.max(0, Math.min(100, Math.round(Number(percent || 0))));
|
||||
|
||||
if (!progress || !bar) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (progressText) {
|
||||
progressText.textContent = text || 'Uploading...';
|
||||
}
|
||||
|
||||
progress.hidden = false;
|
||||
bar.style.width = (isIndeterminate ? 100 : normalizedPercent) + '%';
|
||||
bar.textContent = isIndeterminate ? 'Uploading...' : normalizedPercent + '%';
|
||||
bar.setAttribute('aria-valuenow', String(isIndeterminate ? 100 : normalizedPercent));
|
||||
bar.classList.toggle('progress-bar-striped', Boolean(isIndeterminate));
|
||||
bar.classList.toggle('progress-bar-animated', Boolean(isIndeterminate));
|
||||
}
|
||||
|
||||
function setUploadState(input, isUploading, percent, text, isIndeterminate) {
|
||||
var zone = getUploadZone(input);
|
||||
var progress = getUploadProgressNode(zone);
|
||||
|
||||
if (!zone || !progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
zone.classList.toggle('is-uploading', Boolean(isUploading));
|
||||
zone.setAttribute('aria-busy', isUploading ? 'true' : 'false');
|
||||
input.disabled = Boolean(isUploading);
|
||||
|
||||
if (isUploading) {
|
||||
setUploadProgress(zone, percent, text, isIndeterminate);
|
||||
return;
|
||||
}
|
||||
|
||||
progress.hidden = true;
|
||||
if (input) {
|
||||
input.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setInputFile(input, file) {
|
||||
if (!input || !file) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
input.files = dataTransfer.files;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
function queueUploadCleanup(uploadPaths) {
|
||||
var normalizedPaths = Array.from(new Set((uploadPaths || []).map(function (value) {
|
||||
return String(value || '').trim();
|
||||
}).filter(Boolean)));
|
||||
|
||||
if (!normalizedPaths.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof callbacks.queueUploadCleanup === 'function') {
|
||||
callbacks.queueUploadCleanup(normalizedPaths);
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = JSON.stringify({ uploadPaths: normalizedPaths });
|
||||
var url = String(callbacks.cleanupUrl || '/slides/uploads/cleanup');
|
||||
|
||||
if (navigator.sendBeacon) {
|
||||
try {
|
||||
navigator.sendBeacon(url, new Blob([payload], { type: 'application/json' }));
|
||||
return;
|
||||
} catch (_error) {
|
||||
// Fall back to fetch below.
|
||||
}
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: payload,
|
||||
credentials: 'same-origin',
|
||||
keepalive: true
|
||||
}).catch(function () {
|
||||
// Best-effort cleanup only.
|
||||
});
|
||||
}
|
||||
|
||||
function getPendingUploadCleanupPaths() {
|
||||
return Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"]')).map(function (input) {
|
||||
if (!input || String(input.dataset.uploadedNeedsCleanup || '') !== '1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return String(input.dataset.uploadedPath || '').trim();
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function clearPendingUploadCleanupPaths() {
|
||||
Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"]')).forEach(function (input) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete input.dataset.uploadedNeedsCleanup;
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(input, file) {
|
||||
if (!input || !file) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
var mediaType = getMediaTypeFromInput(input);
|
||||
var regionId = input.name.replace(/^region_(?:image|video)_/, '');
|
||||
var card = getCard(regionId);
|
||||
var hidden = card && card.querySelector('input[type="hidden"][name="' + getExistingMediaPrefix(mediaType) + regionId + '"]');
|
||||
var zone = getUploadZone(input);
|
||||
var uploadToken = String(Date.now()) + ':' + Math.random().toString(16).slice(2);
|
||||
var previousUploadedPath = String(input.dataset.uploadedPath || '').trim();
|
||||
var previousNeedsCleanup = String(input.dataset.uploadedNeedsCleanup || '') === '1';
|
||||
var uploadUrl = String(callbacks.uploadUrl || '/slides/uploads');
|
||||
var uploadTimeoutMs = Math.max(1, Number(callbacks.uploadTimeoutMs || 30000));
|
||||
|
||||
input.dataset.uploadToken = uploadToken;
|
||||
setUploadState(input, true, 0, 'Uploading...', true);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
var formData = new FormData();
|
||||
formData.append('file', file, file.name || 'upload');
|
||||
|
||||
xhr.open('POST', uploadUrl, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.withCredentials = true;
|
||||
xhr.timeout = uploadTimeoutMs;
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
|
||||
|
||||
xhr.upload.onprogress = function (event) {
|
||||
if (input.dataset.uploadToken !== uploadToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event || !event.lengthComputable || !event.total) {
|
||||
setUploadProgress(zone, 100, 'Uploading...', true);
|
||||
return;
|
||||
}
|
||||
|
||||
var percent = Math.round((event.loaded / event.total) * 100);
|
||||
setUploadProgress(zone, percent, 'Uploading ' + percent + '%', false);
|
||||
};
|
||||
|
||||
xhr.onload = function () {
|
||||
if (input.dataset.uploadToken !== uploadToken) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = {
|
||||
ok: xhr.status >= 200 && xhr.status < 300,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
responseText: xhr.responseText || ''
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
var error = new Error(response.responseText || 'Unable to upload media.');
|
||||
error.status = response.status;
|
||||
error.responseText = response.responseText;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (String(response.responseText || '').trim().toLowerCase().indexOf('<!doctype html') === 0 || String(response.responseText || '').indexOf('<html') !== -1) {
|
||||
reject(new Error('Upload redirected to an HTML page. Please sign in again and retry.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
try {
|
||||
payload = JSON.parse(response.responseText || '{}') || {};
|
||||
} catch (_error) {
|
||||
reject(new Error('Unable to parse the upload response.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var uploadedPath = String(payload.path || '').trim();
|
||||
if (!uploadedPath) {
|
||||
reject(new Error('Unable to upload media.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
hidden.value = uploadedPath;
|
||||
}
|
||||
input.dataset.uploadedPath = uploadedPath;
|
||||
input.dataset.uploadedNeedsCleanup = '1';
|
||||
input.value = '';
|
||||
input.setCustomValidity('');
|
||||
|
||||
if (typeof callbacks.renderPreview === 'function') {
|
||||
callbacks.renderPreview(card, uploadedPath);
|
||||
}
|
||||
|
||||
if (typeof callbacks.syncDuration === 'function') {
|
||||
callbacks.syncDuration(card, uploadedPath);
|
||||
}
|
||||
|
||||
if (typeof callbacks.markEdited === 'function') {
|
||||
callbacks.markEdited();
|
||||
}
|
||||
|
||||
if (typeof callbacks.requestPreviewRender === 'function') {
|
||||
callbacks.requestPreviewRender();
|
||||
}
|
||||
|
||||
if (previousNeedsCleanup && previousUploadedPath && previousUploadedPath !== uploadedPath) {
|
||||
queueUploadCleanup([previousUploadedPath]);
|
||||
}
|
||||
|
||||
setUploadState(input, false);
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
if (input.dataset.uploadToken !== uploadToken) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Unable to upload media.'));
|
||||
};
|
||||
|
||||
xhr.onabort = function () {
|
||||
if (input.dataset.uploadToken !== uploadToken) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Unable to upload media.'));
|
||||
};
|
||||
|
||||
xhr.ontimeout = function () {
|
||||
if (input.dataset.uploadToken !== uploadToken) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Upload timed out. Please try again.'));
|
||||
};
|
||||
|
||||
xhr.onloadend = function () {
|
||||
if (input.dataset.uploadToken === uploadToken) {
|
||||
delete input.dataset.uploadToken;
|
||||
setUploadState(input, false);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}).catch(function (error) {
|
||||
if (input.dataset.uploadToken === uploadToken) {
|
||||
delete input.dataset.uploadToken;
|
||||
setUploadState(input, false);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function handleSelection(input, file) {
|
||||
if (typeof callbacks.handleMediaSelection !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return callbacks.handleMediaSelection(input, file, {
|
||||
setInputFile: setInputFile,
|
||||
uploadFile: uploadFile,
|
||||
getMediaTypeFromInput: getMediaTypeFromInput,
|
||||
getExistingMediaPrefix: getExistingMediaPrefix,
|
||||
getMediaPrefix: getMediaPrefix,
|
||||
getUploadZone: getUploadZone,
|
||||
queueUploadCleanup: queueUploadCleanup,
|
||||
getPendingUploadCleanupPaths: getPendingUploadCleanupPaths,
|
||||
clearPendingUploadCleanupPaths: clearPendingUploadCleanupPaths
|
||||
});
|
||||
}
|
||||
|
||||
if (!templateFields.dataset.regionMediaUploadBound) {
|
||||
templateFields.dataset.regionMediaUploadBound = '1';
|
||||
|
||||
templateFields.addEventListener('change', function (event) {
|
||||
var input = event.target && event.target.matches && event.target.matches('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"]') ? event.target : null;
|
||||
|
||||
if (!input || !templateFields.contains(input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var file = input.files && input.files.length ? input.files[0] : null;
|
||||
var hookResult = handleSelection(input, file);
|
||||
|
||||
if (hookResult === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hookResult && typeof hookResult.then === 'function') {
|
||||
hookResult.catch(function (error) {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
input.value = '';
|
||||
if (typeof callbacks.onUploadError === 'function') {
|
||||
callbacks.onUploadError(error, input);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
uploadFile(input, file).catch(function (error) {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
input.value = '';
|
||||
if (typeof callbacks.onUploadError === 'function') {
|
||||
callbacks.onUploadError(error, input);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
templateFields.addEventListener('dragover', function (event) {
|
||||
var zone = event.target && event.target.closest ? event.target.closest('[data-region-upload-zone]') : null;
|
||||
if (!zone || !templateFields.contains(zone)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
zone.classList.add('is-dragover');
|
||||
});
|
||||
|
||||
templateFields.addEventListener('dragleave', function (event) {
|
||||
var zone = event.target && event.target.closest ? event.target.closest('[data-region-upload-zone]') : null;
|
||||
if (!zone || !templateFields.contains(zone)) {
|
||||
return;
|
||||
}
|
||||
|
||||
zone.classList.remove('is-dragover');
|
||||
});
|
||||
|
||||
templateFields.addEventListener('drop', function (event) {
|
||||
var zone = event.target && event.target.closest ? event.target.closest('[data-region-upload-zone]') : null;
|
||||
|
||||
if (!zone || !templateFields.contains(zone)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
zone.classList.remove('is-dragover');
|
||||
|
||||
var input = zone.querySelector('input[type="file"]');
|
||||
var files = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files : null;
|
||||
var file = files && files.length ? files[0] : null;
|
||||
|
||||
if (!input || !file) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hookResult = handleSelection(input, file);
|
||||
|
||||
if (hookResult === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hookResult && typeof hookResult.then === 'function') {
|
||||
hookResult.catch(function (error) {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
input.value = '';
|
||||
if (typeof callbacks.onUploadError === 'function') {
|
||||
callbacks.onUploadError(error, input);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInputFile(input, file);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
uploadFile: uploadFile,
|
||||
queueUploadCleanup: queueUploadCleanup,
|
||||
getPendingUploadCleanupPaths: getPendingUploadCleanupPaths,
|
||||
clearPendingUploadCleanupPaths: clearPendingUploadCleanupPaths,
|
||||
setInputFile: setInputFile
|
||||
};
|
||||
}
|
||||
|
||||
window.pulseRegionUtils = {
|
||||
escapeHtml: escapeHtml,
|
||||
sanitizePreviewHtml: sanitizePreviewHtml,
|
||||
sanitizeRichText: sanitizeRichText,
|
||||
sanitizeFontFamily: sanitizeFontFamily,
|
||||
sanitizeTextColor: sanitizeTextColor,
|
||||
normalizeAcceptList: normalizeAcceptList,
|
||||
fileMatchesAccept: fileMatchesAccept,
|
||||
bindRegionMediaRemoveControls: bindRegionMediaRemoveControls,
|
||||
createRegionMediaUploadController: createRegionMediaUploadController
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,309 @@
|
||||
// API region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
var placeholderUtils = window.placeholderUtils || {};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function sanitizeRichText(html) {
|
||||
return utils.sanitizeRichText ? utils.sanitizeRichText(html) : escapeHtml(html);
|
||||
}
|
||||
|
||||
function sanitizeFontFamily(value) {
|
||||
return utils.sanitizeFontFamily ? utils.sanitizeFontFamily(value) : String(value || '').trim();
|
||||
}
|
||||
|
||||
function sanitizeTextColor(value) {
|
||||
return utils.sanitizeTextColor ? utils.sanitizeTextColor(value) : String(value || '').trim() || '#000000';
|
||||
}
|
||||
|
||||
function sanitizeFontSize(value) {
|
||||
var raw = String(value || '').trim().toLowerCase();
|
||||
if (/^\d+(?:\.\d+)?px$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw.replace(/px$/, '')))));
|
||||
}
|
||||
if (/^\d+(?:\.\d+)?$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw))));
|
||||
}
|
||||
return '32';
|
||||
}
|
||||
|
||||
function getSourceById(sourceId, sources) {
|
||||
var normalizedId = Number(sourceId || 0);
|
||||
return (Array.isArray(sources) ? sources : []).find(function (source) {
|
||||
return Number(source.id) === normalizedId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getSourceItemsPath(source, overridePath) {
|
||||
if (overridePath === undefined || overridePath === null) {
|
||||
return String(source && (source.items_path || source.itemsPath) || '').trim();
|
||||
}
|
||||
|
||||
return String(overridePath || '').trim();
|
||||
}
|
||||
|
||||
function getItems(sourceId, sources, itemsPathOverride) {
|
||||
var source = getSourceById(sourceId, sources);
|
||||
var responseJson = source && source.responseJson && typeof source.responseJson === 'object' ? source.responseJson : null;
|
||||
var itemsPath = getSourceItemsPath(source, itemsPathOverride);
|
||||
if (itemsPath !== undefined && itemsPath !== null && itemsPath !== '') {
|
||||
var current = responseJson;
|
||||
String(itemsPath).split('.').forEach(function (segment) {
|
||||
if (current === undefined || current === null) {
|
||||
current = '';
|
||||
return;
|
||||
}
|
||||
current = current[segment];
|
||||
});
|
||||
return Array.isArray(current) ? current : [];
|
||||
}
|
||||
if (Array.isArray(responseJson)) {
|
||||
return responseJson;
|
||||
}
|
||||
if (responseJson && Array.isArray(responseJson.items)) {
|
||||
return responseJson.items;
|
||||
}
|
||||
if (responseJson && Array.isArray(responseJson.results)) {
|
||||
return responseJson.results;
|
||||
}
|
||||
if (responseJson && Array.isArray(responseJson.data)) {
|
||||
return responseJson.data;
|
||||
}
|
||||
return responseJson ? [responseJson] : [];
|
||||
}
|
||||
|
||||
function getItemCount(sourceId, sources, itemsPathOverride) {
|
||||
return getItems(sourceId, sources, itemsPathOverride).length;
|
||||
}
|
||||
|
||||
function getCurrentConfig(region, existingContent, sources) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var source = getSourceById(current.source_id, sources);
|
||||
var parsedItemNumber = Math.max(1, Number(current.item_number || 1));
|
||||
return {
|
||||
source_id: current.source_id === undefined || current.source_id === null || current.source_id === '' ? '' : Number(current.source_id),
|
||||
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
||||
items_path: Object.prototype.hasOwnProperty.call(current, 'items_path') ? String(current.items_path === undefined || current.items_path === null ? '' : current.items_path) : undefined,
|
||||
default_items_path: String(source && (source.items_path || source.itemsPath) || '').trim(),
|
||||
value: current.value !== undefined ? current.value : ''
|
||||
};
|
||||
}
|
||||
|
||||
function substituteVariables(html, item) {
|
||||
var source = String(html || '');
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof placeholderUtils.resolvePlaceholderExpression !== 'function' || typeof placeholderUtils.formatPlaceholderValue !== 'function') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(item, expression)));
|
||||
});
|
||||
}
|
||||
|
||||
function getItem(sourceId, itemNumber, sources, itemsPathOverride) {
|
||||
var items = getItems(sourceId, sources, itemsPathOverride);
|
||||
var parsedItemNumber = Math.max(1, Number(itemNumber || 1));
|
||||
var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0;
|
||||
return items[index] || null;
|
||||
}
|
||||
|
||||
function getPreviewFallback(item) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
var title = String(item.title || item.name || '').trim();
|
||||
var description = String(item.description || item.summary || item.text || '').trim();
|
||||
var summary = [];
|
||||
if (title) {
|
||||
summary.push('<h3>' + escapeHtml(title) + '</h3>');
|
||||
}
|
||||
if (description) {
|
||||
summary.push('<div>' + sanitizeRichText(description) + '</div>');
|
||||
}
|
||||
if (!summary.length) {
|
||||
return '';
|
||||
}
|
||||
return summary.join('');
|
||||
}
|
||||
|
||||
function normalizeRenderableValue(value) {
|
||||
if (value && typeof value === 'object') {
|
||||
if (value.value !== undefined) {
|
||||
return value.value;
|
||||
}
|
||||
if (value.text !== undefined) {
|
||||
return value.text;
|
||||
}
|
||||
if (value.html !== undefined) {
|
||||
return value.html;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderPreview(region, regionContent, context) {
|
||||
var existingContent = context && context.existingContent ? context.existingContent : {};
|
||||
var sources = context && context.sources ? context.sources : [];
|
||||
var content = normalizeRenderableValue(regionContent && regionContent.value !== undefined ? regionContent.value : '');
|
||||
var config = getCurrentConfig(region, existingContent, sources);
|
||||
var sourceId = regionContent && regionContent.source_id !== undefined ? regionContent.source_id : config.source_id;
|
||||
var itemNumber = regionContent && regionContent.item_number !== undefined ? regionContent.item_number : config.item_number;
|
||||
var itemsPath = regionContent && regionContent.items_path !== undefined ? regionContent.items_path : (config.items_path !== undefined ? config.items_path : config.default_items_path);
|
||||
var itemCount = getItemCount(sourceId, sources, itemsPath);
|
||||
var itemNumberMax = Math.max(1, Number(itemCount || 1));
|
||||
var fontFamily = sanitizeFontFamily(regionContent.font_family || region.fontFamily);
|
||||
var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize);
|
||||
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor);
|
||||
var item = getItem(sourceId, itemNumber, sources, itemsPath);
|
||||
var body = item ? substituteVariables(content, item) : '';
|
||||
|
||||
if (!body && item) {
|
||||
body = getPreviewFallback(item);
|
||||
}
|
||||
|
||||
body = String(body || '')
|
||||
.replace(/<pre[^>]*class="[^"]*api-region-sample-preview[^"]*"[^>]*>[\s\S]*?<\/pre>/gi, '')
|
||||
.replace(/<details[^>]*class="[^"]*api-region-sample-accordion[^"]*"[^>]*>[\s\S]*?<\/details>/gi, '');
|
||||
|
||||
if (!body) {
|
||||
return '';
|
||||
}
|
||||
var renderedBody = sanitizeRichText(body);
|
||||
return renderedBody ? '<div class="template-region api" style="width:100%;height:100%;overflow:hidden;font-family:' + escapeHtml(fontFamily) + ';font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;">' + renderedBody + '</div></div>' : '';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var config = context.config || {};
|
||||
var placeholderChips = String(context.placeholderChips || '');
|
||||
var sourceOptions = String(context.sourceOptions || '');
|
||||
var itemsPathValue = context.itemsPath !== undefined
|
||||
? String(context.itemsPath === null ? '' : context.itemsPath)
|
||||
: String(config.default_items_path || '');
|
||||
var sampleDataPreview = String(context.sampleDataPreview || '');
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" style="position:relative;overflow:visible !important;">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<div class="template-field-actions">' +
|
||||
'<span class="chip">API</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid gap-3 pb-0">' +
|
||||
'<div class="editor-holder" data-region-id="' + region.id + '">' +
|
||||
'<textarea class="editor-source" rows="10">' + escapeHtml(current.value !== undefined ? current.value : '') + '</textarea>' +
|
||||
'</div>' +
|
||||
'<div class="row g-3 align-items-end">' +
|
||||
'<div class="col-12 col-lg-5">' +
|
||||
'<label class="form-label" for="region_api_source_id_' + region.id + '">API source</label>' +
|
||||
'<select id="region_api_source_id_' + region.id + '" name="region_api_source_id_' + region.id + '" class="form-select">' +
|
||||
'<option value="">Select a source</option>' +
|
||||
sourceOptions +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="col-6 col-lg-2">' +
|
||||
'<label class="form-label" for="region_api_item_number_' + region.id + '">Item number</label>' +
|
||||
'<input id="region_api_item_number_' + region.id + '" type="number" min="1" step="1" name="region_api_item_number_' + region.id + '" class="form-control" value="' + escapeHtml(config.item_number) + '" />' +
|
||||
'</div>' +
|
||||
'<div class="col-12 col-lg-5">' +
|
||||
'<label class="form-label" for="region_api_items_path_' + region.id + '">Items path override</label>' +
|
||||
'<div class="input-group flex-nowrap">' +
|
||||
'<input id="region_api_items_path_' + region.id + '" type="text" name="region_api_items_path_' + region.id + '" class="form-control" value="' + escapeHtml(itemsPathValue) + '" placeholder="Leave blank to use the base/root JSON" />' +
|
||||
'<button type="button" class="btn btn-outline-secondary text-nowrap" data-api-items-path-reset data-api-default-items-path="' + escapeHtml(config.default_items_path || '') + '">Reset</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(context.fontSize || '') + '" />' +
|
||||
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current.value !== undefined ? current.value : '') + '" />' +
|
||||
'<div class="muted slide-image-file">Use transforms like {{name.upper()}}, {{name.title()}}, or {{name.lower()}} on leaf fields.</div>' +
|
||||
'<div class="api-region-placeholder-section">' +
|
||||
'<div class="api-region-placeholder-title">Available placeholders</div>' +
|
||||
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + (placeholderChips || '<span class="muted slide-image-file">No JSON fields available.</span>') + '</div>' +
|
||||
'</div>' +
|
||||
'<details class="api-region-sample-accordion" data-api-sample-data-accordion>' +
|
||||
'<summary>Data</summary>' +
|
||||
'<div class="api-region-sample-body" data-api-sample-data-panel>' + sampleDataPreview + '</div>' +
|
||||
'</details>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context, existingContent, sources) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var config = context.config || {};
|
||||
var placeholderChips = String(context.placeholderChips || '');
|
||||
var sourceOptions = String(context.sourceOptions || '');
|
||||
var itemsPathValue = context.itemsPath !== undefined
|
||||
? String(context.itemsPath === null ? '' : context.itemsPath)
|
||||
: String(config.default_items_path || '');
|
||||
var sampleDataPreview = String(context.sampleDataPreview || '');
|
||||
|
||||
return {
|
||||
region: region,
|
||||
current: current,
|
||||
config: config,
|
||||
fontSize: context.fontSize || '',
|
||||
itemsPath: itemsPathValue,
|
||||
sampleDataPreview: sampleDataPreview,
|
||||
placeholderChips: placeholderChips,
|
||||
sourceOptions: sourceOptions,
|
||||
existingContent: existingContent || {},
|
||||
sources: Array.isArray(sources) ? sources : []
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent, sources) {
|
||||
var current = getCurrentConfig(region, existingContent || {}, sources || []);
|
||||
var editor = window.tinymce && typeof window.tinymce.get === 'function' ? window.tinymce.get('slide-editor-region-' + region.id) : null;
|
||||
var content = card && card.querySelector ? {
|
||||
value: String(editor ? editor.getContent({ format: 'html' }) : ((card.querySelector('textarea[name="region_text_' + region.id + '"]') || {}).value || current.value || '')),
|
||||
source_id: (card.querySelector('select[name="region_api_source_id_' + region.id + '"]') || {}).value || current.source_id,
|
||||
item_number: (card.querySelector('input[name="region_api_item_number_' + region.id + '"]') || {}).value || current.item_number,
|
||||
items_path: card.querySelector('input[name="region_api_items_path_' + region.id + '"]') ? (card.querySelector('input[name="region_api_items_path_' + region.id + '"]') || {}).value : current.items_path
|
||||
} : current;
|
||||
|
||||
return {
|
||||
value: content.value,
|
||||
source_id: content.source_id,
|
||||
item_number: content.item_number,
|
||||
items_path: content.items_path,
|
||||
existingContent: existingContent || {},
|
||||
sources: Array.isArray(sources) ? sources : []
|
||||
};
|
||||
}
|
||||
|
||||
registry.register('api', {
|
||||
label: 'API',
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 240 };
|
||||
},
|
||||
getSourceById: getSourceById,
|
||||
getItems: getItems,
|
||||
getItemCount: getItemCount,
|
||||
getCurrentConfig: getCurrentConfig,
|
||||
getItem: getItem,
|
||||
getPreviewFallback: getPreviewFallback,
|
||||
substituteVariables: substituteVariables,
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard,
|
||||
buildEditorCardContext: buildEditorCardContext,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,46 @@
|
||||
// HTML region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function renderPreview(value) {
|
||||
var content = value && typeof value === 'object' && value.value !== undefined ? value : null;
|
||||
var html = String(content ? content.value : value || '').trim();
|
||||
if (!html) {
|
||||
return '<div class="slide-preview-placeholder">HTML</div>';
|
||||
}
|
||||
return '<iframe class="slide-preview-html-frame" sandbox="" scrolling="no" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="HTML preview" loading="eager" style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"></iframe>';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = String(context.current || '');
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">HTML</span>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid">' +
|
||||
'<label style="display:block;">HTML content' +
|
||||
'<textarea name="region_html_' + region.id + '" class="form-control" rows="10" placeholder="<div>Hello</div>">' + escapeHtml(current) + '</textarea>' +
|
||||
'</label>' +
|
||||
'<div class="muted slide-image-file">HTML is rendered in a sandboxed iframe preview.</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
registry.register('html', {
|
||||
label: 'HTML',
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 240 };
|
||||
},
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,150 @@
|
||||
// Image region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function showUploadWarning(message) {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert(message);
|
||||
}
|
||||
|
||||
function renderPreview(region, value) {
|
||||
var content = value && typeof value === 'object' && value.value !== undefined ? value : null;
|
||||
var src = String(content ? content.value : value || '').trim();
|
||||
if (!src) {
|
||||
return '<div class="slide-preview-placeholder">Image</div>';
|
||||
}
|
||||
return '<img class="slide-preview-image" src="' + escapeHtml(src) + '" alt="" style="width:100%;height:100%;object-fit:contain;display:block;" />';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = String(context.current || '');
|
||||
var regionRatio = String(context.regionRatio || '1:1');
|
||||
var uploadConfig = context.uploadConfig || {};
|
||||
var uploadMaxLabel = String(uploadConfig.limitLabel || context.uploadMaxLabel || '100 MB');
|
||||
var uploadAccept = Array.isArray(uploadConfig.accept) ? uploadConfig.accept.join(', ') : String(uploadConfig.accept || 'image/png, image/jpeg, image/gif, image/webp');
|
||||
var uploadHelpText = String(uploadConfig.helpText || 'PNG, JPG, GIF, or WebP');
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" data-region-media-type="image">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Image</span>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid">' +
|
||||
'<div class="row g-3 align-items-start">' +
|
||||
'<div class="col-12 col-md-8 d-flex flex-column">' +
|
||||
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" for="region_image_' + region.id + '">' +
|
||||
'<input type="file" id="region_image_' + region.id + '" name="region_image_' + region.id + '" class="visually-hidden" accept="' + escapeHtml(uploadAccept) + '" data-slide-image-cropper-region-ratio="' + escapeHtml(regionRatio) + '" data-slide-image-cropper-region-ratio-label="Region" />' +
|
||||
'<span class="slide-image-region-upload-zone-content">' +
|
||||
'<span class="slide-image-region-upload-zone-icon"><i class="bi bi-cloud-arrow-up" aria-hidden="true"></i></span>' +
|
||||
'<span class="slide-image-region-upload-zone-copy">' +
|
||||
'<strong>Drop an image here or click to upload</strong>' +
|
||||
'<span>' + escapeHtml(uploadHelpText) + '</span>' +
|
||||
'<span class="slide-image-region-upload-zone-limit">Max ' + escapeHtml(uploadMaxLabel) + ' per file</span>' +
|
||||
'</span>' +
|
||||
'</span>' +
|
||||
'<span class="slide-image-region-upload-zone-progress" data-region-upload-progress hidden>' +
|
||||
'<span class="slide-image-region-upload-zone-progress-label" data-region-upload-progress-text>Uploading...</span>' +
|
||||
'<div class="progress slide-image-region-upload-zone-progress-bar" role="progressbar" aria-label="Image upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
|
||||
'<div class="progress-bar bg-danger progress-bar-striped progress-bar-animated" data-region-upload-progress-bar style="width:0%">0%</div>' +
|
||||
'</div>' +
|
||||
'</span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="col-12 col-md-4">' +
|
||||
'<div class="slide-image-region-preview-box">' +
|
||||
(current
|
||||
? '<div class="slide-image-region-preview-shell" data-remove-region-image="' + region.id + '" role="button" tabindex="0" aria-label="Remove image">' +
|
||||
'<img class="slide-image-region-preview" src="' + escapeHtml(current) + '" alt="Current image preview" />' +
|
||||
'<span class="slide-image-region-preview-remove" aria-hidden="true"><i class="bi bi-trash3" aria-hidden="true"></i></span>' +
|
||||
'</div>'
|
||||
: '<div class="slide-image-region-preview slide-image-region-preview-empty">No image</div>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="existing_region_image_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context) {
|
||||
var defaultConfig = context && context.uploadConfig ? context.uploadConfig : {};
|
||||
return {
|
||||
region: context.region,
|
||||
current: String(context.current || ''),
|
||||
regionRatio: String(context.regionRatio || '1:1'),
|
||||
uploadConfig: {
|
||||
accept: Array.isArray(defaultConfig.accept) ? defaultConfig.accept : ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
|
||||
limitBytes: defaultConfig.limitBytes,
|
||||
limitLabel: defaultConfig.limitLabel || context.uploadMaxLabel || '100 MB',
|
||||
helpText: defaultConfig.helpText || 'PNG, JPG, GIF, or WebP'
|
||||
},
|
||||
uploadMaxLabel: context.uploadMaxLabel || '100 MB'
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent) {
|
||||
var current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
var imageHidden = card && card.querySelector ? card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]') : null;
|
||||
var imageFileInput = card && card.querySelector ? card.querySelector('input[type="file"][name="region_image_' + region.id + '"]') : null;
|
||||
|
||||
return {
|
||||
value: imageFileInput && imageFileInput.dataset.previewUrl ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : (current && current.value !== undefined ? current.value : '')),
|
||||
existingContent: existingContent || {}
|
||||
};
|
||||
}
|
||||
|
||||
function handleMediaInputChange(context) {
|
||||
if (!context || !context.file || typeof context.uploadFile !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var uploadConfig = context.uploadConfig || {};
|
||||
var accept = uploadConfig.accept || ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
||||
var limitBytes = Number(uploadConfig.limitBytes || 100 * 1024 * 1024);
|
||||
|
||||
if (!utils.fileMatchesAccept || !utils.fileMatchesAccept(context.file, accept)) {
|
||||
showUploadWarning('This image region accepts PNG, JPG, GIF, or WebP files.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number(context.file.size || 0) > limitBytes) {
|
||||
showUploadWarning('File must be ' + String(uploadConfig.limitLabel || '100 MB') + ' or smaller.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.uploadFile(context.file);
|
||||
}
|
||||
|
||||
registry.register('image', {
|
||||
label: 'Image',
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 240 };
|
||||
},
|
||||
getMediaUploadConfig: function (context) {
|
||||
var defaultConfig = context && context.defaultConfig ? context.defaultConfig : {};
|
||||
|
||||
return {
|
||||
accept: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
|
||||
limitBytes: defaultConfig.limitBytes,
|
||||
limitLabel: defaultConfig.limitLabel,
|
||||
helpText: 'PNG, JPG, GIF, or WebP'
|
||||
};
|
||||
},
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard,
|
||||
buildEditorCardContext: buildEditorCardContext,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext,
|
||||
handleMediaInputChange: handleMediaInputChange
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,299 @@
|
||||
// RSS region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
var DEFAULT_STYLE = {
|
||||
font_family: 'Arial',
|
||||
font_size: 32,
|
||||
font_color: '#000000'
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function sanitizePreviewHtml(html) {
|
||||
return utils.sanitizePreviewHtml ? utils.sanitizePreviewHtml(html) : escapeHtml(html);
|
||||
}
|
||||
|
||||
function sanitizeRichText(html) {
|
||||
return utils.sanitizeRichText ? utils.sanitizeRichText(html) : sanitizePreviewHtml(html);
|
||||
}
|
||||
|
||||
function sanitizeFontFamily(value) {
|
||||
return utils.sanitizeFontFamily ? utils.sanitizeFontFamily(value) : String(value || '').trim();
|
||||
}
|
||||
|
||||
function sanitizeTextColor(value) {
|
||||
return utils.sanitizeTextColor ? utils.sanitizeTextColor(value) : String(value || '').trim() || '#000000';
|
||||
}
|
||||
|
||||
function sanitizeFontSize(value) {
|
||||
var raw = String(value || '').trim().toLowerCase();
|
||||
if (/^\d+(?:\.\d+)?px$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw.replace(/px$/, '')))));
|
||||
}
|
||||
if (/^\d+(?:\.\d+)?$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw))));
|
||||
}
|
||||
return '32';
|
||||
}
|
||||
|
||||
function getDefaultStyle() {
|
||||
return {
|
||||
font_family: DEFAULT_STYLE.font_family,
|
||||
font_size: DEFAULT_STYLE.font_size,
|
||||
font_color: DEFAULT_STYLE.font_color
|
||||
};
|
||||
}
|
||||
|
||||
function getFeedById(feedId, feeds) {
|
||||
var normalizedId = Number(feedId || 0);
|
||||
return (Array.isArray(feeds) ? feeds : []).find(function (feed) {
|
||||
return Number(feed.id) === normalizedId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getFieldList(feedId, feeds) {
|
||||
var feed = getFeedById(feedId, feeds);
|
||||
var sampleItem = feed && Array.isArray(feed.items) ? feed.items[0] : null;
|
||||
var sampleJson = sampleItem && sampleItem.itemJson && typeof sampleItem.itemJson === 'object' ? sampleItem.itemJson : null;
|
||||
|
||||
function walkFields(value, prefix, output) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
if (key === 'rawXml') {
|
||||
return;
|
||||
}
|
||||
|
||||
var nextPath = prefix ? prefix + '.' + key : key;
|
||||
var nextValue = value[key];
|
||||
if (nextValue && typeof nextValue === 'object' && !Array.isArray(nextValue)) {
|
||||
walkFields(nextValue, nextPath, output);
|
||||
return;
|
||||
}
|
||||
|
||||
if (output.indexOf(nextPath) === -1) {
|
||||
output.push(nextPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var fields = [];
|
||||
walkFields(sampleJson, '', fields);
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getCurrentConfig(region, existingContent) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var parsedItemNumber = Math.max(1, Number(current.item_number || 1));
|
||||
return {
|
||||
feed_id: current.feed_id === undefined || current.feed_id === null || current.feed_id === '' ? '' : Number(current.feed_id),
|
||||
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
||||
value: current.value !== undefined ? current.value : ''
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePath(value, path) {
|
||||
var current = value;
|
||||
if (!path) {
|
||||
return current;
|
||||
}
|
||||
String(path).split('.').forEach(function (segment) {
|
||||
if (current === undefined || current === null) {
|
||||
current = '';
|
||||
return;
|
||||
}
|
||||
current = current[segment];
|
||||
});
|
||||
return current === undefined || current === null ? '' : current;
|
||||
}
|
||||
|
||||
function substituteVariables(html, item) {
|
||||
var source = String(html || '');
|
||||
return source.replace(/\{\{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_.]+))?\s*\}\}/g, function (_match, tokenName, tokenPath) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return '';
|
||||
}
|
||||
var key = tokenPath ? tokenName + '.' + tokenPath : tokenName;
|
||||
return escapeHtml(resolvePath(item, key || ''));
|
||||
});
|
||||
}
|
||||
|
||||
function getItem(feedId, itemNumber, feeds) {
|
||||
var feed = getFeedById(feedId, feeds);
|
||||
var items = feed && Array.isArray(feed.items) ? feed.items : [];
|
||||
var parsedItemNumber = Math.max(1, Number(itemNumber || 1));
|
||||
var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0;
|
||||
return items[index] || null;
|
||||
}
|
||||
|
||||
function getPreviewFallback(item) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return '<div class="slide-preview-placeholder">RSS item</div>';
|
||||
}
|
||||
|
||||
var title = String(item.title || '').trim();
|
||||
var description = String(item.description || '').trim();
|
||||
var summary = [];
|
||||
if (title) {
|
||||
summary.push('<h3>' + escapeHtml(title) + '</h3>');
|
||||
}
|
||||
if (description) {
|
||||
summary.push('<p>' + sanitizePreviewHtml(description) + '</p>');
|
||||
}
|
||||
if (!summary.length) {
|
||||
return '<div class="slide-preview-placeholder">RSS item</div>';
|
||||
}
|
||||
return summary.join('');
|
||||
}
|
||||
|
||||
function normalizeRenderableValue(value) {
|
||||
if (value && typeof value === 'object') {
|
||||
if (value.value !== undefined) {
|
||||
return value.value;
|
||||
}
|
||||
if (value.text !== undefined) {
|
||||
return value.text;
|
||||
}
|
||||
if (value.html !== undefined) {
|
||||
return value.html;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderPreview(region, regionContent, context) {
|
||||
var existingContent = context && context.existingContent ? context.existingContent : {};
|
||||
var feeds = context && context.feeds ? context.feeds : [];
|
||||
var content = normalizeRenderableValue(regionContent && regionContent.value !== undefined ? regionContent.value : '');
|
||||
var config = getCurrentConfig(region, existingContent);
|
||||
var defaultStyle = getDefaultStyle();
|
||||
var feedId = regionContent && regionContent.feed_id !== undefined ? regionContent.feed_id : config.feed_id;
|
||||
var itemNumber = regionContent && regionContent.item_number !== undefined ? regionContent.item_number : config.item_number;
|
||||
var fontFamily = sanitizeFontFamily(regionContent.font_family || region.fontFamily || defaultStyle.font_family);
|
||||
var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize || defaultStyle.font_size);
|
||||
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor || defaultStyle.font_color);
|
||||
var item = getItem(feedId, itemNumber, feeds);
|
||||
var body = item ? substituteVariables(content, item) : '';
|
||||
|
||||
if (!body && item) {
|
||||
var summaryParts = [];
|
||||
if (item.title) {
|
||||
summaryParts.push('<h3>' + escapeHtml(item.title) + '</h3>');
|
||||
}
|
||||
if (item.description) {
|
||||
summaryParts.push('<div>' + sanitizeRichText(item.description) + '</div>');
|
||||
}
|
||||
body = summaryParts.join('');
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
return '';
|
||||
}
|
||||
var renderedBody = sanitizeRichText(body);
|
||||
return renderedBody ? '<div class="template-region rss" style="width:100%;height:100%;overflow:hidden;font-family:' + escapeHtml(fontFamily) + ';font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;">' + renderedBody + '</div></div>' : '';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var config = context.config || {};
|
||||
var placeholderChips = String(context.placeholderChips || '');
|
||||
var feedOptions = String(context.feedOptions || '');
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" style="position:relative;overflow:visible !important;">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<div class="template-field-actions">' +
|
||||
'<span class="chip">RSS</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid">' +
|
||||
'<div class="editor-holder" data-region-id="' + region.id + '">' +
|
||||
'<textarea class="editor-source" rows="10">' + escapeHtml(current.value !== undefined ? current.value : '') + '</textarea>' +
|
||||
'</div>' +
|
||||
'<div class="row g-3 align-items-end">' +
|
||||
'<div class="col-12 col-md-8">' +
|
||||
'<label class="form-label" for="region_rss_feed_id_' + region.id + '">RSS feed</label>' +
|
||||
'<select id="region_rss_feed_id_' + region.id + '" name="region_rss_feed_id_' + region.id + '" class="form-select">' +
|
||||
'<option value="">Select a feed</option>' +
|
||||
feedOptions +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="col-6 col-md-4">' +
|
||||
'<label class="form-label" for="region_rss_item_number_' + region.id + '">Entry number</label>' +
|
||||
'<input id="region_rss_item_number_' + region.id + '" type="number" min="1" step="1" name="region_rss_item_number_' + region.id + '" class="form-control" value="' + escapeHtml(config.item_number) + '" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(context.fontSize || '') + '" />' +
|
||||
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current.value !== undefined ? current.value : '') + '" />' +
|
||||
'<div class="muted slide-image-file">Use placeholders like {{title.upper()}}, {{title.title()}}, or {{title.lower()}}. Available placeholders:</div>' +
|
||||
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + (placeholderChips || '<span class="muted slide-image-file">No RSS fields available.</span>') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context, existingContent, feeds) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var config = context.config || {};
|
||||
var placeholderChips = String(context.placeholderChips || '');
|
||||
var feedOptions = String(context.feedOptions || '');
|
||||
|
||||
return {
|
||||
region: region,
|
||||
current: current,
|
||||
config: config,
|
||||
fontSize: context.fontSize || '',
|
||||
placeholderChips: placeholderChips,
|
||||
feedOptions: feedOptions,
|
||||
existingContent: existingContent || {},
|
||||
feeds: Array.isArray(feeds) ? feeds : []
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent, feeds) {
|
||||
var current = getCurrentConfig(region, existingContent || {});
|
||||
var editor = window.tinymce && typeof window.tinymce.get === 'function' ? window.tinymce.get('slide-editor-region-' + region.id) : null;
|
||||
var content = card && card.querySelector ? {
|
||||
value: String(editor ? editor.getContent({ format: 'html' }) : ((card.querySelector('textarea[name="region_text_' + region.id + '"]') || {}).value || current.value || '')),
|
||||
feed_id: (card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]') || {}).value || current.feed_id,
|
||||
item_number: (card.querySelector('input[name="region_rss_item_number_' + region.id + '"]') || {}).value || current.item_number
|
||||
} : current;
|
||||
|
||||
return {
|
||||
value: content.value,
|
||||
feed_id: content.feed_id,
|
||||
item_number: content.item_number,
|
||||
existingContent: existingContent || {},
|
||||
feeds: Array.isArray(feeds) ? feeds : []
|
||||
};
|
||||
}
|
||||
|
||||
registry.register('rss', {
|
||||
label: 'RSS',
|
||||
getDefaultStyle: getDefaultStyle,
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 240 };
|
||||
},
|
||||
getFieldList: getFieldList,
|
||||
getCurrentConfig: getCurrentConfig,
|
||||
getItem: getItem,
|
||||
getPreviewFallback: getPreviewFallback,
|
||||
substituteVariables: substituteVariables,
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard,
|
||||
buildEditorCardContext: buildEditorCardContext,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,52 @@
|
||||
// RTMP region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function renderPreview(value, disableAudio) {
|
||||
var content = value && typeof value === 'object' && value.value !== undefined ? value : null;
|
||||
var src = String(content ? content.value : value || '').trim();
|
||||
var label = src ? 'RTMP' : 'RTMP stream';
|
||||
if (disableAudio) {
|
||||
label += ' (muted)';
|
||||
}
|
||||
return '<div class="slide-preview-placeholder" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;">' + label + '</div>';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = String(context.current || '');
|
||||
var disableAudio = context.disableAudio === undefined ? true : Boolean(context.disableAudio);
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">RTMP</span>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid">' +
|
||||
'<label style="display:block;">RTMP URL' +
|
||||
'<input type="url" name="region_rtmp_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="rtmp://example.com/live/stream" />' +
|
||||
'</label>' +
|
||||
'<label class="form-check m-0">' +
|
||||
'<input class="form-check-input" type="checkbox" name="region_disable_audio_' + region.id + '" value="1"' + (disableAudio ? ' checked' : '') + ' />' +
|
||||
'<span class="form-check-label">Disable audio</span>' +
|
||||
'</label>' +
|
||||
'<div class="muted slide-image-file">Player controls stay hidden and the stream is not interactive.</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
registry.register('rtmp', {
|
||||
label: 'RTMP',
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 240 };
|
||||
},
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,104 @@
|
||||
// Text region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
var DEFAULT_STYLE = {
|
||||
font_family: 'Arial',
|
||||
font_size: 32,
|
||||
font_color: '#000000'
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function sanitizePreviewHtml(html) {
|
||||
return utils.sanitizePreviewHtml ? utils.sanitizePreviewHtml(html) : escapeHtml(html);
|
||||
}
|
||||
|
||||
function getDefaultStyle() {
|
||||
return {
|
||||
font_family: DEFAULT_STYLE.font_family,
|
||||
font_size: DEFAULT_STYLE.font_size,
|
||||
font_color: DEFAULT_STYLE.font_color
|
||||
};
|
||||
}
|
||||
|
||||
function renderPreview(region, value, styleOrContext) {
|
||||
var rawValue = value && typeof value === 'object' && value.value !== undefined ? value.value : value;
|
||||
var raw = String(rawValue || '');
|
||||
if (!raw) {
|
||||
return '<div class="slide-preview-placeholder">Empty text</div>';
|
||||
}
|
||||
|
||||
var style = styleOrContext && styleOrContext.style ? styleOrContext.style : styleOrContext || {};
|
||||
var fontFamily = style && style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '';
|
||||
var color = style && style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '';
|
||||
var fontSize = style && style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '';
|
||||
var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1));
|
||||
var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1));
|
||||
var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;' + fontFamily + fontSize + color;
|
||||
return '<div class="slide-preview-text-content" style="' + wrapperStyle + '">' + sanitizePreviewHtml(raw) + '</div>';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = String(context.current || '');
|
||||
var style = context.style || {};
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" style="position:relative;overflow:visible !important;">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<div class="template-field-actions">' +
|
||||
'<span class="chip">Text</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid">' +
|
||||
'<div class="editor-holder" data-region-id="' + region.id + '">' +
|
||||
'<textarea class="editor-source" rows="10">' + escapeHtml(current) + '</textarea>' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context) {
|
||||
return {
|
||||
region: context.region,
|
||||
current: String(context.current || ''),
|
||||
style: context.style || {}
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent) {
|
||||
var current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
var textarea = card && card.querySelector ? card.querySelector('textarea.editor-source') : null;
|
||||
var hidden = card && card.querySelector ? card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]') : null;
|
||||
var editor = window.tinymce && typeof window.tinymce.get === 'function' ? window.tinymce.get('slide-editor-region-' + region.id) : null;
|
||||
var defaultStyle = getDefaultStyle();
|
||||
var style = current && typeof current === 'object' ? {
|
||||
font_family: current.font_family || region.font_family || defaultStyle.font_family,
|
||||
font_size: current.font_size || defaultStyle.font_size,
|
||||
font_color: current.font_color || region.font_color || defaultStyle.font_color
|
||||
} : {};
|
||||
|
||||
return {
|
||||
value: String(editor ? editor.getContent({ format: 'html' }) : (hidden && hidden.value !== undefined ? hidden.value : (textarea && textarea.value !== undefined ? textarea.value : (current && current.value !== undefined ? current.value : '')))),
|
||||
style: style,
|
||||
existingContent: existingContent || {}
|
||||
};
|
||||
}
|
||||
|
||||
registry.register('text', {
|
||||
label: 'Text',
|
||||
getDefaultStyle: getDefaultStyle,
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 300, height: 120 };
|
||||
},
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard,
|
||||
buildEditorCardContext: buildEditorCardContext,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,357 @@
|
||||
// Time/date region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
var placeholderUtils = window.placeholderUtils || {};
|
||||
var placeholderChips = window.placeholderChips || {};
|
||||
var DEFAULT_STYLE = {
|
||||
font_family: 'Arial',
|
||||
font_size: 32,
|
||||
font_color: '#000000'
|
||||
};
|
||||
var COMMON_TIME_ZONES = [
|
||||
'UTC',
|
||||
'Europe/London',
|
||||
'Europe/Dublin',
|
||||
'Europe/Paris',
|
||||
'Europe/Berlin',
|
||||
'Europe/Madrid',
|
||||
'America/New_York',
|
||||
'America/Chicago',
|
||||
'America/Denver',
|
||||
'America/Los_Angeles',
|
||||
'America/Toronto',
|
||||
'America/Vancouver',
|
||||
'America/Sao_Paulo',
|
||||
'Asia/Dubai',
|
||||
'Asia/Kolkata',
|
||||
'Asia/Singapore',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Seoul',
|
||||
'Australia/Sydney',
|
||||
'Pacific/Auckland'
|
||||
];
|
||||
var formatterCache = Object.create(null);
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function sanitizePreviewHtml(html) {
|
||||
return utils.sanitizePreviewHtml ? utils.sanitizePreviewHtml(html) : escapeHtml(html);
|
||||
}
|
||||
|
||||
function normalizeEditorText(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /gi, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getDefaultStyle() {
|
||||
return {
|
||||
font_family: DEFAULT_STYLE.font_family,
|
||||
font_size: DEFAULT_STYLE.font_size,
|
||||
font_color: DEFAULT_STYLE.font_color
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultTimeZone() {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
} catch (_error) {
|
||||
return 'UTC';
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTimeZone(value) {
|
||||
var raw = String(value || '').trim();
|
||||
if (!raw) {
|
||||
return getDefaultTimeZone();
|
||||
}
|
||||
|
||||
try {
|
||||
new Intl.DateTimeFormat('en-GB', { timeZone: raw }).format(new Date());
|
||||
return raw;
|
||||
} catch (_error) {
|
||||
return getDefaultTimeZone();
|
||||
}
|
||||
}
|
||||
|
||||
function getFormatter(cacheKey, options) {
|
||||
if (!formatterCache[cacheKey]) {
|
||||
formatterCache[cacheKey] = new Intl.DateTimeFormat('en-GB', options);
|
||||
}
|
||||
|
||||
return formatterCache[cacheKey];
|
||||
}
|
||||
|
||||
function getFormattedParts(timeZone, date) {
|
||||
var targetDate = date instanceof Date ? date : new Date();
|
||||
var resolvedTimeZone = resolveTimeZone(timeZone);
|
||||
var numericParts = getFormatter('numeric:' + resolvedTimeZone, {
|
||||
timeZone: resolvedTimeZone,
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
}).formatToParts(targetDate);
|
||||
var weekdayLong = getFormatter('weekday-long:' + resolvedTimeZone, {
|
||||
timeZone: resolvedTimeZone,
|
||||
weekday: 'long'
|
||||
}).formatToParts(targetDate);
|
||||
var weekdayShort = getFormatter('weekday-short:' + resolvedTimeZone, {
|
||||
timeZone: resolvedTimeZone,
|
||||
weekday: 'short'
|
||||
}).formatToParts(targetDate);
|
||||
var monthLong = getFormatter('month-long:' + resolvedTimeZone, {
|
||||
timeZone: resolvedTimeZone,
|
||||
month: 'long'
|
||||
}).formatToParts(targetDate);
|
||||
var monthShort = getFormatter('month-short:' + resolvedTimeZone, {
|
||||
timeZone: resolvedTimeZone,
|
||||
month: 'short'
|
||||
}).formatToParts(targetDate);
|
||||
var ampm = getFormatter('ampm:' + resolvedTimeZone, {
|
||||
timeZone: resolvedTimeZone,
|
||||
hour12: true,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).formatToParts(targetDate);
|
||||
|
||||
function getPart(parts, type) {
|
||||
var match = parts.find(function (part) {
|
||||
return part && part.type === type;
|
||||
});
|
||||
return match ? String(match.value || '') : '';
|
||||
}
|
||||
|
||||
function toTitleCase(value) {
|
||||
return String(value || '').toLowerCase().replace(/\b([a-z])/g, function (match, letter) {
|
||||
return String(letter || '').toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
h: String(Number(getPart(numericParts, 'hour')) || 0),
|
||||
hh: getPart(numericParts, 'hour'),
|
||||
m: String(Number(getPart(numericParts, 'minute')) || 0),
|
||||
mm: getPart(numericParts, 'minute'),
|
||||
s: String(Number(getPart(numericParts, 'second')) || 0),
|
||||
ss: getPart(numericParts, 'second'),
|
||||
d: String(Number(getPart(numericParts, 'day')) || 0),
|
||||
dd: getPart(numericParts, 'day'),
|
||||
M: String(Number(getPart(numericParts, 'month')) || 0),
|
||||
MM: getPart(numericParts, 'month'),
|
||||
y: String(Number(getPart(numericParts, 'year')) || 0),
|
||||
yyyy: getPart(numericParts, 'year'),
|
||||
yy: String(Number(String(getPart(numericParts, 'year')).slice(-2)) || 0).toString().padStart(2, '0'),
|
||||
ddd: toTitleCase(getPart(weekdayShort, 'weekday')),
|
||||
dddd: toTitleCase(getPart(weekdayLong, 'weekday')),
|
||||
MMM: toTitleCase(getPart(monthShort, 'month')),
|
||||
MMMM: toTitleCase(getPart(monthLong, 'month')),
|
||||
a: toTitleCase(getPart(ampm, 'dayPeriod')),
|
||||
tz: resolvedTimeZone,
|
||||
tz_short: getPart(getFormatter('tz-short:' + resolvedTimeZone, {
|
||||
timeZone: resolvedTimeZone,
|
||||
timeZoneName: 'short'
|
||||
}).formatToParts(targetDate), 'timeZoneName'),
|
||||
date: getPart(numericParts, 'year') + '-' + getPart(numericParts, 'month') + '-' + getPart(numericParts, 'day'),
|
||||
time: getPart(numericParts, 'hour') + ':' + getPart(numericParts, 'minute') + ':' + getPart(numericParts, 'second')
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTimeDatePlaceholder(values, expression) {
|
||||
if (typeof placeholderUtils.resolvePlaceholderExpression === 'function' && typeof placeholderUtils.formatPlaceholderValue === 'function') {
|
||||
return placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(values, expression));
|
||||
}
|
||||
|
||||
var parsed = String(expression || '').trim();
|
||||
return Object.prototype.hasOwnProperty.call(values, parsed) ? values[parsed] : '';
|
||||
}
|
||||
|
||||
function renderTimeDatePlaceholderChips() {
|
||||
if (window.timeDatePlaceholders && typeof window.timeDatePlaceholders.renderChips === 'function') {
|
||||
return window.timeDatePlaceholders.renderChips();
|
||||
}
|
||||
|
||||
var tokens = [
|
||||
'h',
|
||||
'hh',
|
||||
'm',
|
||||
'mm',
|
||||
's',
|
||||
'ss',
|
||||
'd',
|
||||
'dd',
|
||||
'M',
|
||||
'MM',
|
||||
'y',
|
||||
'yyyy',
|
||||
'yy',
|
||||
'ddd',
|
||||
'dddd',
|
||||
'MMM',
|
||||
'MMMM',
|
||||
'a',
|
||||
'tz',
|
||||
'tz_short'
|
||||
];
|
||||
|
||||
if (typeof placeholderChips.renderChips === 'function') {
|
||||
return placeholderChips.renderChips(tokens);
|
||||
}
|
||||
|
||||
return tokens.map(function (token) {
|
||||
return '<span class="chip">{{' + token + '}}</span>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getEditorContentStyle() {
|
||||
return 'body { text-align: center; }';
|
||||
}
|
||||
|
||||
function renderTemplate(format, timeZone, date) {
|
||||
var template = String(format || '').trim();
|
||||
var values = getFormattedParts(timeZone, date);
|
||||
return template.replace(/\{\{\s*([a-zA-Z0-9_.()\-]+)\s*\}\}/g, function (_match, key) {
|
||||
return String(resolveTimeDatePlaceholder(values, key) || '');
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentValue(region, existingContent) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
return {
|
||||
value: current.value !== undefined && current.value !== null ? String(current.value) : (current.text !== undefined && current.text !== null ? String(current.text) : ''),
|
||||
timezone: current.timezone !== undefined ? current.timezone : (current.time_zone !== undefined ? current.time_zone : getDefaultTimeZone())
|
||||
};
|
||||
}
|
||||
|
||||
function getTextStyle(region, current) {
|
||||
var defaultStyle = getDefaultStyle();
|
||||
return {
|
||||
font_family: current.font_family || region.font_family || defaultStyle.font_family,
|
||||
font_size: current.font_size || defaultStyle.font_size,
|
||||
font_color: current.font_color || region.font_color || defaultStyle.font_color
|
||||
};
|
||||
}
|
||||
|
||||
function renderPreview(region, regionContent, style, scale) {
|
||||
var current = regionContent && typeof regionContent === 'object' ? regionContent : {};
|
||||
var format = current.value !== undefined && current.value !== null
|
||||
? String(current.value)
|
||||
: (current.text !== undefined && current.text !== null ? String(current.text) : '');
|
||||
var timeZone = resolveTimeZone(current.timezone || current.time_zone || '');
|
||||
var textStyle = getTextStyle(region, current);
|
||||
var fontFamily = textStyle.font_family ? 'font-family:' + escapeHtml(textStyle.font_family) + ';' : '';
|
||||
var fontSize = textStyle.font_size ? 'font-size:' + Math.max(1, Math.round(Number(textStyle.font_size))) + 'px;' : '';
|
||||
var fontColor = textStyle.font_color ? 'color:' + escapeHtml(textStyle.font_color) + ';' : '';
|
||||
var rendered = renderTemplate(format, timeZone, new Date());
|
||||
var previewScale = Math.max(0.01, Number(scale || 1) || 1);
|
||||
var previewWidth = Math.max(1, Math.round(Number(region && (region.width || region.pixelWidth) ? (region.width || region.pixelWidth) : 0) || 1));
|
||||
var previewHeight = Math.max(1, Math.round(Number(region && (region.height || region.pixelHeight) ? (region.height || region.pixelHeight) : 0) || 1));
|
||||
var contentStyle = 'width:' + previewWidth + 'px;height:' + previewHeight + 'px;transform:scale(' + previewScale + ');transform-origin:top left;' + (fontFamily ? fontFamily : '') + fontSize + fontColor + 'white-space:pre-wrap;line-height:1.1;font-variant-numeric:tabular-nums;';
|
||||
return '<div class="template-region time-date" data-time-date-format="' + escapeHtml(format) + '" data-time-date-timezone="' + escapeHtml(timeZone) + '" style="width:100%;height:100%;overflow:hidden;"><div class="template-region-text-scale" style="' + contentStyle + '">' + sanitizePreviewHtml(rendered) + '</div></div>';
|
||||
}
|
||||
|
||||
function buildTimezoneOptionsMarkup(selectedTimeZone) {
|
||||
var supportedValues = COMMON_TIME_ZONES;
|
||||
if (Intl.supportedValuesOf) {
|
||||
try {
|
||||
supportedValues = Intl.supportedValuesOf('timeZone');
|
||||
} catch (_error) {
|
||||
supportedValues = COMMON_TIME_ZONES;
|
||||
}
|
||||
}
|
||||
|
||||
var seen = Object.create(null);
|
||||
var options = [];
|
||||
[selectedTimeZone].concat(supportedValues).forEach(function (timeZone) {
|
||||
var value = String(timeZone || '').trim();
|
||||
if (!value || seen[value]) {
|
||||
return;
|
||||
}
|
||||
seen[value] = true;
|
||||
options.push('<option value="' + escapeHtml(value) + '"></option>');
|
||||
});
|
||||
return options.join('');
|
||||
}
|
||||
|
||||
function renderPlaceholderChips() {
|
||||
return renderTimeDatePlaceholderChips();
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var value = current.value !== undefined && current.value !== null
|
||||
? String(current.value)
|
||||
: (current.text !== undefined && current.text !== null ? String(current.text) : '');
|
||||
var timeZone = resolveTimeZone(current.timezone || current.time_zone || '');
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" style="position:relative;overflow:visible !important;">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<div class="template-field-actions">' +
|
||||
'<span class="chip">Time / Date</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid gap-3">' +
|
||||
'<div class="editor-holder" data-region-id="' + region.id + '">' +
|
||||
'<textarea id="region_time_date_text_' + region.id + '" class="editor-source form-control" rows="4" name="region_text_' + region.id + '">' + escapeHtml(value) + '</textarea>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="form-label" for="region_time_date_timezone_' + region.id + '">Time zone</label>' +
|
||||
'<input id="region_time_date_timezone_' + region.id + '" type="text" name="region_timezone_' + region.id + '" class="form-control" value="' + escapeHtml(timeZone) + '" list="region_time_date_timezones_' + region.id + '" placeholder="Europe/London" autocomplete="off" />' +
|
||||
'<datalist id="region_time_date_timezones_' + region.id + '">' + buildTimezoneOptionsMarkup(timeZone) + '</datalist>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<div class="api-region-placeholder-title mb-2">Available placeholders</div>' +
|
||||
'<div class="d-flex flex-wrap gap-2">' + renderPlaceholderChips() + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context) {
|
||||
var current = context.current || {};
|
||||
return {
|
||||
region: context.region,
|
||||
current: {
|
||||
value: current.value !== undefined && current.value !== null ? String(current.value) : (current.text !== undefined && current.text !== null ? String(current.text) : ''),
|
||||
timezone: resolveTimeZone(current.timezone || current.time_zone || '')
|
||||
},
|
||||
style: context.style || {}
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent) {
|
||||
var current = getCurrentValue(region, existingContent || {});
|
||||
var textarea = card && card.querySelector ? card.querySelector('textarea[name="region_text_' + region.id + '"]') : null;
|
||||
var timezoneInput = card && card.querySelector ? card.querySelector('input[name="region_timezone_' + region.id + '"]') : null;
|
||||
return {
|
||||
value: String(textarea && textarea.value !== undefined ? textarea.value : (current.value !== undefined && current.value !== null ? current.value : (current.text !== undefined && current.text !== null ? current.text : ''))),
|
||||
timezone: String(timezoneInput && timezoneInput.value !== undefined ? timezoneInput.value : (current.timezone || getDefaultTimeZone())),
|
||||
existingContent: existingContent || {},
|
||||
style: getTextStyle(region, current)
|
||||
};
|
||||
}
|
||||
|
||||
registry.register('time-date', {
|
||||
label: 'Time / Date',
|
||||
getDefaultStyle: getDefaultStyle,
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 180 };
|
||||
},
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard,
|
||||
getEditorContentStyle: getEditorContentStyle,
|
||||
buildEditorCardContext: buildEditorCardContext,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,181 @@
|
||||
// Video region helpers for editor previews and duration probing.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function showUploadWarning(message) {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert(message);
|
||||
}
|
||||
|
||||
function renderPreview(region, value) {
|
||||
var content = value && typeof value === 'object' && value.value !== undefined ? value : null;
|
||||
var src = String(content ? content.value : value || '').trim();
|
||||
if (!src) {
|
||||
return '<div class="slide-preview-placeholder">Video</div>';
|
||||
}
|
||||
return '<video class="slide-preview-video" src="' + escapeHtml(src) + '" autoplay loop muted playsinline preload="metadata" style="width:100%;height:100%;object-fit:contain;display:block;"></video>';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = String(context.current || '');
|
||||
var currentDuration = String(context.currentDuration || '');
|
||||
var uploadConfig = context.uploadConfig || {};
|
||||
var uploadVideoMaxLabel = String(uploadConfig.limitLabel || context.uploadVideoMaxLabel || '1 GB');
|
||||
var uploadAccept = Array.isArray(uploadConfig.accept) ? uploadConfig.accept.join(', ') : String(uploadConfig.accept || 'video/mp4, video/webm, video/ogg');
|
||||
var uploadHelpText = String(uploadConfig.helpText || 'MP4, WebM, or Ogg');
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" data-region-media-type="video">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Video</span>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid">' +
|
||||
'<div class="row g-3 align-items-start">' +
|
||||
'<div class="col-12 col-md-8 d-flex flex-column">' +
|
||||
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" for="region_video_' + region.id + '">' +
|
||||
'<input type="file" id="region_video_' + region.id + '" name="region_video_' + region.id + '" class="visually-hidden" accept="' + escapeHtml(uploadAccept) + '" />' +
|
||||
'<span class="slide-image-region-upload-zone-content">' +
|
||||
'<span class="slide-image-region-upload-zone-icon"><i class="bi bi-camera-video" aria-hidden="true"></i></span>' +
|
||||
'<span class="slide-image-region-upload-zone-copy">' +
|
||||
'<strong>Drop a video here or click to upload</strong>' +
|
||||
'<span>' + escapeHtml(uploadHelpText) + '</span>' +
|
||||
'<span class="slide-image-region-upload-zone-limit">Max ' + escapeHtml(uploadVideoMaxLabel) + ' per file</span>' +
|
||||
'</span>' +
|
||||
'</span>' +
|
||||
'<span class="slide-image-region-upload-zone-progress" data-region-upload-progress hidden>' +
|
||||
'<span class="slide-image-region-upload-zone-progress-label" data-region-upload-progress-text>Uploading...</span>' +
|
||||
'<div class="progress slide-image-region-upload-zone-progress-bar" role="progressbar" aria-label="Video upload progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
|
||||
'<div class="progress-bar bg-danger progress-bar-striped progress-bar-animated" data-region-upload-progress-bar style="width:0%">0%</div>' +
|
||||
'</div>' +
|
||||
'</span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="col-12 col-md-4">' +
|
||||
'<div class="slide-image-region-preview-box">' +
|
||||
(current
|
||||
? '<div class="slide-image-region-preview-shell" data-remove-region-video="' + region.id + '" role="button" tabindex="0" aria-label="Remove video">' +
|
||||
'<video class="slide-image-region-preview" src="' + escapeHtml(current) + '" muted playsinline preload="metadata"></video>' +
|
||||
'<span class="slide-image-region-preview-remove" aria-hidden="true"><i class="bi bi-trash3" aria-hidden="true"></i></span>' +
|
||||
'</div>'
|
||||
: '<div class="slide-image-region-preview slide-image-region-preview-empty">No video</div>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="existing_region_video_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
||||
'<input type="hidden" name="existing_region_video_duration_' + region.id + '" value="' + escapeHtml(currentDuration) + '" />' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context) {
|
||||
var defaultConfig = context && context.uploadConfig ? context.uploadConfig : {};
|
||||
return {
|
||||
region: context.region,
|
||||
current: String(context.current || ''),
|
||||
currentDuration: String(context.currentDuration || ''),
|
||||
uploadConfig: {
|
||||
accept: Array.isArray(defaultConfig.accept) ? defaultConfig.accept : ['video/mp4', 'video/webm', 'video/ogg'],
|
||||
limitBytes: defaultConfig.limitBytes,
|
||||
limitLabel: defaultConfig.limitLabel || context.uploadVideoMaxLabel || '1 GB',
|
||||
helpText: defaultConfig.helpText || 'MP4, WebM, or Ogg'
|
||||
},
|
||||
uploadVideoMaxLabel: context.uploadVideoMaxLabel || '1 GB'
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent) {
|
||||
var current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||
var videoHidden = card && card.querySelector ? card.querySelector('input[type="hidden"][name="existing_region_video_' + region.id + '"]') : null;
|
||||
var videoFileInput = card && card.querySelector ? card.querySelector('input[type="file"][name="region_video_' + region.id + '"]') : null;
|
||||
|
||||
return {
|
||||
value: videoFileInput && videoFileInput.dataset.previewUrl ? videoFileInput.dataset.previewUrl : (videoHidden ? videoHidden.value : (current && current.value !== undefined ? current.value : '')),
|
||||
existingContent: existingContent || {}
|
||||
};
|
||||
}
|
||||
|
||||
function handleMediaInputChange(context) {
|
||||
if (!context || !context.file || typeof context.uploadFile !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var uploadConfig = context.uploadConfig || {};
|
||||
var accept = uploadConfig.accept || ['video/mp4', 'video/webm', 'video/ogg'];
|
||||
var limitBytes = Number(uploadConfig.limitBytes || 1024 * 1024 * 1024);
|
||||
|
||||
if (!utils.fileMatchesAccept || !utils.fileMatchesAccept(context.file, accept)) {
|
||||
showUploadWarning('This video region accepts MP4, WebM, or Ogg files.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Number(context.file.size || 0) > limitBytes) {
|
||||
showUploadWarning('File must be ' + String(uploadConfig.limitLabel || '1 GB') + ' or smaller.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.uploadFile(context.file);
|
||||
}
|
||||
|
||||
function syncDuration(card, source, options) {
|
||||
var helpers = options || {};
|
||||
if (!card || typeof helpers.getHiddenInput !== 'function' || typeof helpers.loadDuration !== 'function') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var hidden = helpers.getHiddenInput(card);
|
||||
if (!hidden) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var mediaPath = String(source || '').trim();
|
||||
if (!mediaPath) {
|
||||
hidden.value = '';
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return helpers.loadDuration(mediaPath).then(function (duration) {
|
||||
hidden.value = String(duration);
|
||||
}).catch(function () {
|
||||
hidden.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
registry.register('video', {
|
||||
label: 'Video',
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 240 };
|
||||
},
|
||||
getMediaUploadConfig: function (context) {
|
||||
var defaultConfig = context && context.defaultConfig ? context.defaultConfig : {};
|
||||
|
||||
return {
|
||||
accept: ['video/mp4', 'video/webm', 'video/ogg'],
|
||||
limitBytes: defaultConfig.limitBytes,
|
||||
limitLabel: defaultConfig.limitLabel,
|
||||
helpText: 'MP4, WebM, or Ogg'
|
||||
};
|
||||
},
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard,
|
||||
buildEditorCardContext: buildEditorCardContext,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext,
|
||||
handleMediaInputChange: handleMediaInputChange,
|
||||
syncDuration: syncDuration
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,46 @@
|
||||
// Webpage region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function renderPreview(value) {
|
||||
var content = value && typeof value === 'object' && value.value !== undefined ? value : null;
|
||||
var src = String(content ? content.value : value || '').trim();
|
||||
if (!src) {
|
||||
return '<div class="slide-preview-placeholder">Webpage</div>';
|
||||
}
|
||||
return '<iframe class="slide-preview-webpage-frame" src="' + escapeHtml(src) + '" title="Webpage preview" loading="eager" referrerpolicy="no-referrer" scrolling="no" style="width:100%;height:100%;border:0;display:block;background:#fff;overflow:hidden;"></iframe>';
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = String(context.current || '');
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Webpage</span>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid">' +
|
||||
'<label style="display:block;">Webpage URL' +
|
||||
'<input type="url" name="region_webpage_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
||||
'</label>' +
|
||||
'<div class="muted slide-image-file">The webpage will be loaded in an iframe.</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
registry.register('webpage', {
|
||||
label: 'Webpage',
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 420, height: 240 };
|
||||
},
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard
|
||||
});
|
||||
}());
|
||||
@@ -3,6 +3,41 @@
|
||||
return;
|
||||
}
|
||||
|
||||
function updateBackgroundTasksCardHeight() {
|
||||
var card = document.getElementById('background-tasks-task-card');
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cardRect = card.getBoundingClientRect();
|
||||
var bottomInset = 16;
|
||||
var availableHeight = Math.max(0, window.innerHeight - cardRect.top - bottomInset);
|
||||
card.style.setProperty('--background-tasks-task-card-max-height', availableHeight + 'px');
|
||||
}
|
||||
|
||||
var scheduleUpdate = window.requestAnimationFrame ? function () {
|
||||
window.requestAnimationFrame(updateBackgroundTasksCardHeight);
|
||||
} : updateBackgroundTasksCardHeight;
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('resize', scheduleUpdate);
|
||||
window.addEventListener('orientationchange', scheduleUpdate);
|
||||
window.addEventListener('load', scheduleUpdate);
|
||||
}
|
||||
|
||||
if (window.ResizeObserver) {
|
||||
try {
|
||||
var summary = document.getElementById('background-tasks-summary');
|
||||
if (summary) {
|
||||
new ResizeObserver(scheduleUpdate).observe(summary);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore ResizeObserver setup failures.
|
||||
}
|
||||
}
|
||||
|
||||
scheduleUpdate();
|
||||
|
||||
try {
|
||||
var url = new URL(window.location.href);
|
||||
if (!url.searchParams.has('message')) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Shared placeholder chip markup and copy-to-clipboard behavior.
|
||||
|
||||
(function () {
|
||||
var root = window;
|
||||
var CHIP_ATTR = 'data-placeholder-chip';
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function copyTextToClipboard(text) {
|
||||
var value = String(text === undefined || text === null ? '' : text);
|
||||
if (!value) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function' && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(value).then(function () {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.value = value;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-9999px';
|
||||
textarea.style.top = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
var success = false;
|
||||
try {
|
||||
success = document.execCommand('copy');
|
||||
} catch (_error) {
|
||||
success = false;
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
resolve(success);
|
||||
});
|
||||
}
|
||||
|
||||
function findChipElement(target) {
|
||||
if (!target || typeof target.closest !== 'function') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return target.closest('[' + CHIP_ATTR + ']');
|
||||
}
|
||||
|
||||
function activateChipCopy(chip) {
|
||||
if (!chip) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return copyTextToClipboard(chip.getAttribute('data-copy-text') || chip.textContent || '');
|
||||
}
|
||||
|
||||
function handleChipInteraction(event) {
|
||||
var chip = findChipElement(event && event.target);
|
||||
if (!chip) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'keydown') {
|
||||
var key = String(event.key || '');
|
||||
if (key !== 'Enter' && key !== ' ') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
activateChipCopy(chip);
|
||||
}
|
||||
|
||||
function ensureCopyHandler() {
|
||||
if (root.__placeholderChipCopyHandlerInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
root.__placeholderChipCopyHandlerInstalled = true;
|
||||
document.addEventListener('click', handleChipInteraction, true);
|
||||
document.addEventListener('keydown', handleChipInteraction, true);
|
||||
}
|
||||
|
||||
function renderChip(token) {
|
||||
var copyText = '{{' + String(token || '') + '}}';
|
||||
var escapedCopyText = escapeHtml(copyText);
|
||||
ensureCopyHandler();
|
||||
|
||||
return '<span class="chip" ' + CHIP_ATTR + '="1" data-copy-text="' + escapedCopyText + '" role="button" tabindex="0" title="Click to copy ' + escapedCopyText + '" aria-label="Copy ' + escapedCopyText + '">' + escapedCopyText + '</span>';
|
||||
}
|
||||
|
||||
function renderChips(tokens) {
|
||||
var list = Array.isArray(tokens) ? tokens : [];
|
||||
return list.map(function (token) {
|
||||
return renderChip(token);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
root.placeholderChips = {
|
||||
renderChip: renderChip,
|
||||
renderChips: renderChips,
|
||||
copyTextToClipboard: copyTextToClipboard,
|
||||
ensureCopyHandler: ensureCopyHandler
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,131 @@
|
||||
// Shared placeholder parsing and resolution helpers for browser-rendered API content.
|
||||
|
||||
(function () {
|
||||
var root = window;
|
||||
var transformPattern = /^(upper|lower|title)\(\)$/i;
|
||||
|
||||
function resolvePath(value, path) {
|
||||
var current = value;
|
||||
if (!path) {
|
||||
return current;
|
||||
}
|
||||
|
||||
String(path).split('.').forEach(function (segment) {
|
||||
if (current === undefined || current === null) {
|
||||
current = '';
|
||||
return;
|
||||
}
|
||||
|
||||
current = current[segment];
|
||||
});
|
||||
|
||||
return current === undefined || current === null ? '' : current;
|
||||
}
|
||||
|
||||
function parsePlaceholderExpression(expression) {
|
||||
var raw = String(expression || '').trim();
|
||||
var segments = raw ? raw.split('.') : [];
|
||||
var transforms = [];
|
||||
|
||||
while (segments.length) {
|
||||
var candidate = String(segments[segments.length - 1] || '').trim();
|
||||
if (!transformPattern.test(candidate)) {
|
||||
break;
|
||||
}
|
||||
|
||||
transforms.unshift(candidate.replace(/\(\)$/g, '').toLowerCase());
|
||||
segments.pop();
|
||||
}
|
||||
|
||||
return {
|
||||
path: segments.join('.'),
|
||||
transforms: transforms
|
||||
};
|
||||
}
|
||||
|
||||
function applyTransform(value, transform) {
|
||||
var text = String(value === undefined || value === null ? '' : value);
|
||||
|
||||
if (transform === 'lower') {
|
||||
return text.toLowerCase();
|
||||
}
|
||||
|
||||
if (transform === 'upper') {
|
||||
return text.toUpperCase();
|
||||
}
|
||||
|
||||
if (transform === 'title') {
|
||||
return text.toLowerCase().replace(/\b([a-z])/g, function (match, letter) {
|
||||
return String(letter || '').toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function resolvePlaceholderExpression(value, expression) {
|
||||
var parsed = parsePlaceholderExpression(expression);
|
||||
var resolved = resolvePath(value, parsed.path);
|
||||
|
||||
parsed.transforms.forEach(function (transform) {
|
||||
resolved = applyTransform(resolved, transform);
|
||||
});
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function formatPlaceholderValue(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function collectPlaceholderFieldPaths(value) {
|
||||
var output = [];
|
||||
|
||||
function walk(currentValue, prefix) {
|
||||
if (!currentValue || typeof currentValue !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(currentValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(currentValue).forEach(function (key) {
|
||||
var nextPath = prefix ? prefix + '.' + key : key;
|
||||
var nextValue = currentValue[key];
|
||||
|
||||
if (nextValue && typeof nextValue === 'object') {
|
||||
walk(nextValue, nextPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (output.indexOf(nextPath) === -1) {
|
||||
output.push(nextPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
walk(value, '');
|
||||
return output;
|
||||
}
|
||||
|
||||
root.placeholderUtils = {
|
||||
resolvePath: resolvePath,
|
||||
parsePlaceholderExpression: parsePlaceholderExpression,
|
||||
resolvePlaceholderExpression: resolvePlaceholderExpression,
|
||||
formatPlaceholderValue: formatPlaceholderValue,
|
||||
collectPlaceholderFieldPaths: collectPlaceholderFieldPaths
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,48 @@
|
||||
// Shared time/date placeholder definitions used by editor renderers.
|
||||
|
||||
(function () {
|
||||
var root = window;
|
||||
var PLACEHOLDERS = [
|
||||
{ token: 'h', label: 'h' },
|
||||
{ token: 'hh', label: 'hh' },
|
||||
{ token: 'm', label: 'm' },
|
||||
{ token: 'mm', label: 'mm' },
|
||||
{ token: 's', label: 's' },
|
||||
{ token: 'ss', label: 'ss' },
|
||||
{ token: 'd', label: 'd' },
|
||||
{ token: 'dd', label: 'dd' },
|
||||
{ token: 'M', label: 'M' },
|
||||
{ token: 'MM', label: 'MM' },
|
||||
{ token: 'y', label: 'y' },
|
||||
{ token: 'yyyy', label: 'yyyy' },
|
||||
{ token: 'yy', label: 'yy' },
|
||||
{ token: 'ddd', label: 'ddd' },
|
||||
{ token: 'dddd', label: 'dddd' },
|
||||
{ token: 'MMM', label: 'MMM' },
|
||||
{ token: 'MMMM', label: 'MMMM' },
|
||||
{ token: 'a', label: 'a' },
|
||||
{ token: 'tz', label: 'tz' },
|
||||
{ token: 'tz_short', label: 'tz_short' }
|
||||
];
|
||||
|
||||
function getTokens() {
|
||||
return PLACEHOLDERS.slice();
|
||||
}
|
||||
|
||||
function renderChips() {
|
||||
if (root.placeholderChips && typeof root.placeholderChips.renderChips === 'function') {
|
||||
return root.placeholderChips.renderChips(PLACEHOLDERS.map(function (item) {
|
||||
return item.token;
|
||||
}));
|
||||
}
|
||||
|
||||
return PLACEHOLDERS.map(function (item) {
|
||||
return '<span class="chip">{{' + item.token + '}}</span>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
root.timeDatePlaceholders = {
|
||||
getTokens: getTokens,
|
||||
renderChips: renderChips
|
||||
};
|
||||
}());
|
||||
@@ -0,0 +1,332 @@
|
||||
export function createSlideFormEditorController(options) {
|
||||
var settings = options || {};
|
||||
var templateFields = settings.templateFields || null;
|
||||
var templateSelectorLock = settings.templateSelectorLock || null;
|
||||
var requestPreviewRender = typeof settings.requestPreviewRender === 'function' ? settings.requestPreviewRender : function () {};
|
||||
var defaultFontSize = Math.max(1, Number(settings.defaultFontSize || 32));
|
||||
var fontFamilyFormats = String(settings.fontFamilyFormats || '').trim();
|
||||
var fontStylesheetHref = String(settings.fontStylesheetHref || '').trim();
|
||||
var defaultEditorFontFamily = 'Arial, Helvetica, sans-serif';
|
||||
var editorInstances = new Map();
|
||||
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () {
|
||||
return null;
|
||||
};
|
||||
var getEditorBackgroundColor = typeof settings.getEditorBackgroundColor === 'function' ? settings.getEditorBackgroundColor : function () {
|
||||
return '#111111';
|
||||
};
|
||||
|
||||
function normalizeEditorData(value) {
|
||||
return String(value || '');
|
||||
}
|
||||
|
||||
function normalizeEditorMarkup(value) {
|
||||
return String(value === undefined || value === null ? '' : value).trim();
|
||||
}
|
||||
|
||||
function normalizeFontSizeValue(value) {
|
||||
var raw = String(value || '').trim().toLowerCase();
|
||||
if (/^\d+(?:\.\d+)?px$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw.replace(/px$/, '')))));
|
||||
}
|
||||
if (/^\d+(?:\.\d+)?$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw))));
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getEditorHiddenInput(regionId) {
|
||||
if (!templateFields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
||||
return card ? card.querySelector('input[type="hidden"][name="region_text_' + regionId + '"]') : null;
|
||||
}
|
||||
|
||||
function getEditorFontSizeHiddenInput(regionId) {
|
||||
if (!templateFields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
||||
return card ? card.querySelector('input[name="region_font_size_' + regionId + '"]') : null;
|
||||
}
|
||||
|
||||
function getEditorRegionType(card) {
|
||||
if (!card) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (card.dataset && String(card.dataset.regionMediaType || '').trim()) {
|
||||
return String(card.dataset.regionMediaType || '').trim();
|
||||
}
|
||||
|
||||
var regionTypeInput = card.querySelector ? card.querySelector('input[type="hidden"][name="region_type[]"]') : null;
|
||||
return String(regionTypeInput && regionTypeInput.value || '').trim();
|
||||
}
|
||||
|
||||
function getEditorContentStyle(regionType) {
|
||||
var module = getRegionTypeModule(regionType);
|
||||
if (module && typeof module.getEditorContentStyle === 'function') {
|
||||
return String(module.getEditorContentStyle() || '').trim();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function syncEditorFontSizeHidden(regionId) {
|
||||
var fontSizeHidden = getEditorFontSizeHiddenInput(regionId);
|
||||
if (!fontSizeHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
fontSizeHidden.value = normalizeFontSizeValue(fontSizeHidden.value) || String(defaultFontSize);
|
||||
}
|
||||
|
||||
function getTinymce() {
|
||||
return window.tinymce || null;
|
||||
}
|
||||
|
||||
function getThemeName() {
|
||||
var theme = String(document.documentElement && document.documentElement.dataset && document.documentElement.dataset.bsTheme || 'light').trim().toLowerCase();
|
||||
if (theme === 'auto') {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
return theme === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function getTinyMceThemeAssets() {
|
||||
var themeName = getThemeName();
|
||||
var skinName = themeName === 'dark' ? 'oxide-dark' : 'oxide';
|
||||
var skinBase = '/assets/vendor/tinymce/skins/ui/' + skinName;
|
||||
return {
|
||||
themeName: themeName,
|
||||
skinName: skinName,
|
||||
skinUrl: skinBase,
|
||||
contentCss: skinBase + '/content.css',
|
||||
bodyClass: themeName === 'dark' ? 'tinymce-theme-dark' : 'tinymce-theme-light'
|
||||
};
|
||||
}
|
||||
|
||||
function getEditorBackgroundColorValue() {
|
||||
var value = String(getEditorBackgroundColor() || '').trim();
|
||||
return value || '#111111';
|
||||
}
|
||||
|
||||
function getFontFamilyFormats() {
|
||||
if (fontFamilyFormats) {
|
||||
return fontFamilyFormats;
|
||||
}
|
||||
|
||||
return [
|
||||
'Default=inherit',
|
||||
'Arial=Arial,Helvetica,sans-serif',
|
||||
'Comic Sans MS=Comic Sans MS,cursive,sans-serif',
|
||||
'Courier New=Courier New,Courier,monospace',
|
||||
'Georgia=Georgia,serif',
|
||||
'Helvetica=Helvetica,Arial,sans-serif',
|
||||
'Impact=Impact,Charcoal,sans-serif',
|
||||
'Lucida Sans Unicode=Lucida Sans Unicode,Lucida Grande,sans-serif',
|
||||
'Palatino Linotype=Palatino Linotype,Book Antiqua,Palatino,serif',
|
||||
'Tahoma=Tahoma,Geneva,sans-serif',
|
||||
'Times New Roman=Times New Roman,Times,serif',
|
||||
'Trebuchet MS=Trebuchet MS,Helvetica,sans-serif',
|
||||
'Verdana=Verdana,Geneva,sans-serif'
|
||||
].join(';');
|
||||
}
|
||||
|
||||
function getContentCss() {
|
||||
var themeAssets = getTinyMceThemeAssets();
|
||||
return fontStylesheetHref
|
||||
? [themeAssets.contentCss, fontStylesheetHref]
|
||||
: themeAssets.contentCss;
|
||||
}
|
||||
|
||||
function attachEditorEvents(regionId, editor) {
|
||||
var hidden = getEditorHiddenInput(regionId);
|
||||
var source = editor && editor.targetElm ? editor.targetElm : null;
|
||||
|
||||
function syncState() {
|
||||
var currentEditor = editorInstances.get(regionId);
|
||||
if (currentEditor !== editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof editor.save === 'function') {
|
||||
editor.save();
|
||||
}
|
||||
var content = editor.getContent({ format: 'html' });
|
||||
if (hidden) {
|
||||
hidden.value = content;
|
||||
}
|
||||
if (source) {
|
||||
source.value = content;
|
||||
}
|
||||
if (templateSelectorLock && typeof templateSelectorLock.markEdited === 'function') {
|
||||
templateSelectorLock.markEdited();
|
||||
}
|
||||
requestPreviewRender();
|
||||
}
|
||||
|
||||
editor.on('init', function () {
|
||||
var content = editor.getContent({ format: 'html' });
|
||||
if (hidden) {
|
||||
hidden.value = content;
|
||||
}
|
||||
if (source) {
|
||||
source.value = content;
|
||||
}
|
||||
syncEditorFontSizeHidden(regionId);
|
||||
syncState();
|
||||
});
|
||||
|
||||
['change', 'keyup', 'undo', 'redo', 'SetContent', 'Paste', 'input', 'NodeChange'].forEach(function (eventName) {
|
||||
editor.on(eventName, syncState);
|
||||
});
|
||||
}
|
||||
|
||||
function createEditorForHolder(holder) {
|
||||
if (!holder) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
var regionId = holder.getAttribute('data-region-id');
|
||||
var card = holder.closest ? holder.closest('[data-region-id]') : null;
|
||||
var source = holder.querySelector('.editor-source');
|
||||
var hidden = getEditorHiddenInput(regionId);
|
||||
var tinymce = getTinymce();
|
||||
var themeAssets = getTinyMceThemeAssets();
|
||||
var editorContentStyle = getEditorContentStyle(getEditorRegionType(card));
|
||||
|
||||
if (!source || !tinymce || typeof tinymce.init !== 'function') {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
source.value = normalizeEditorMarkup(normalizeEditorData(source && source.value !== undefined && source.value !== null ? source.value : (hidden ? hidden.value : '')));
|
||||
|
||||
if (!source.id) {
|
||||
source.id = 'slide-editor-region-' + regionId;
|
||||
}
|
||||
|
||||
return tinymce.init({
|
||||
target: source,
|
||||
menubar: false,
|
||||
branding: false,
|
||||
promotion: false,
|
||||
statusbar: true,
|
||||
resize: true,
|
||||
plugins: 'lists link code advlist fullscreen',
|
||||
toolbar: 'undo redo | fontfamily fontsizeinput | forecolor backcolor bold italic underline strikethrough subscript superscript removeformat | align lineheight indent outdent bullist numlist | fullscreen',
|
||||
toolbar_mode: 'sliding',
|
||||
license_key: 'gpl',
|
||||
skin: themeAssets.skinName,
|
||||
skin_url: themeAssets.skinUrl,
|
||||
content_css: getContentCss(),
|
||||
body_class: themeAssets.bodyClass,
|
||||
content_style: 'body { font-family: ' + defaultEditorFontFamily + '; font-size: 32px; line-height: 1.5; background-color: ' + getEditorBackgroundColorValue() + '; } p { margin: 1em 0; } p:first-child { margin-top: 0; } p:last-child { margin-bottom: 1em; }' + (editorContentStyle ? ' ' + editorContentStyle : ''),
|
||||
font_family_formats: getFontFamilyFormats(),
|
||||
font_size_input_default_unit: 'px',
|
||||
forced_root_block: 'p',
|
||||
force_br_newlines: false,
|
||||
newline_behavior: 'default',
|
||||
setup: function (editor) {
|
||||
editorInstances.set(regionId, editor);
|
||||
attachEditorEvents(regionId, editor);
|
||||
}
|
||||
}).then(function (editors) {
|
||||
var editor = Array.isArray(editors) && editors.length ? editors[0] : tinymce.get(source.id);
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
editorInstances.set(regionId, editor);
|
||||
if (editor.targetElm) {
|
||||
editor.targetElm.value = editor.getContent({ format: 'html' });
|
||||
}
|
||||
return editor;
|
||||
}).catch(function (error) {
|
||||
console.error('Failed to initialize TinyMCE.', error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function renderEditors() {
|
||||
if (!templateFields) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
watchThemeChanges();
|
||||
|
||||
var holders = Array.prototype.slice.call(templateFields.querySelectorAll('.editor-holder'));
|
||||
return Promise.all(holders.map(function (holder) {
|
||||
return createEditorForHolder(holder);
|
||||
}));
|
||||
}
|
||||
|
||||
function syncEditors() {
|
||||
if (!templateFields) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
var saves = Array.prototype.map.call(templateFields.querySelectorAll('.editor-holder'), function (holder) {
|
||||
var regionId = holder.getAttribute('data-region-id');
|
||||
var editor = editorInstances.get(regionId);
|
||||
var hidden = getEditorHiddenInput(regionId);
|
||||
|
||||
if (!editor || !hidden) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
hidden.value = editor.getContent({ format: 'html' });
|
||||
syncEditorFontSizeHidden(regionId);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return Promise.all(saves);
|
||||
}
|
||||
|
||||
function destroyEditors() {
|
||||
editorInstances.forEach(function (editor) {
|
||||
if (editor && typeof editor.remove === 'function') {
|
||||
editor.remove();
|
||||
}
|
||||
});
|
||||
editorInstances.clear();
|
||||
|
||||
var tinymce = getTinymce();
|
||||
if (tinymce && typeof tinymce.remove === 'function') {
|
||||
tinymce.remove();
|
||||
}
|
||||
}
|
||||
|
||||
var themeObserver = null;
|
||||
|
||||
function watchThemeChanges() {
|
||||
if (themeObserver || !window.MutationObserver || !templateFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
themeObserver = new MutationObserver(function () {
|
||||
destroyEditors();
|
||||
renderEditors();
|
||||
});
|
||||
|
||||
themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-bs-theme']
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
renderEditors: renderEditors,
|
||||
syncEditors: syncEditors,
|
||||
destroyEditors: function () {
|
||||
if (themeObserver) {
|
||||
themeObserver.disconnect();
|
||||
themeObserver = null;
|
||||
}
|
||||
destroyEditors();
|
||||
},
|
||||
watchThemeChanges: watchThemeChanges
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
export function createSlideFormPreviewHelpers(options) {
|
||||
var settings = options || {};
|
||||
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () { return null; };
|
||||
var escapeHtml = typeof settings.escapeHtml === 'function'
|
||||
? settings.escapeHtml
|
||||
: function (value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
function sanitizeTagAttributes(tagName, attrText) {
|
||||
var allowedAttributes = {
|
||||
a: ['href', 'title', 'target', 'rel', 'class', 'style'],
|
||||
blockquote: ['class', 'style'],
|
||||
div: ['class', 'style'],
|
||||
figure: ['class', 'style'],
|
||||
figcaption: ['class', 'style'],
|
||||
h1: ['class', 'style'],
|
||||
h2: ['class', 'style'],
|
||||
h3: ['class', 'style'],
|
||||
h4: ['class', 'style'],
|
||||
h5: ['class', 'style'],
|
||||
h6: ['class', 'style'],
|
||||
li: ['class', 'style'],
|
||||
ol: ['class', 'style', 'start'],
|
||||
p: ['class', 'style'],
|
||||
pre: ['class', 'style'],
|
||||
span: ['class', 'style'],
|
||||
table: ['class', 'style'],
|
||||
td: ['class', 'style', 'colspan', 'rowspan'],
|
||||
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
|
||||
tr: ['class', 'style'],
|
||||
ul: ['class', 'style']
|
||||
};
|
||||
var allowed = allowedAttributes[tagName] || [];
|
||||
if (!allowed.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var attrs = [];
|
||||
attrText.replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, function (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) {
|
||||
var lowerKey = String(key || '').toLowerCase();
|
||||
if (allowed.indexOf(lowerKey) === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : '';
|
||||
if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) {
|
||||
return '';
|
||||
}
|
||||
if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) {
|
||||
return '';
|
||||
}
|
||||
if (lowerKey === 'target') {
|
||||
var targetValue = String(value || '').trim();
|
||||
if (targetValue === '_blank') {
|
||||
attrs.push(' target="_blank"');
|
||||
if (attrs.indexOf(' rel="noreferrer noopener"') === -1) {
|
||||
attrs.push(' rel="noreferrer noopener"');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"');
|
||||
return '';
|
||||
});
|
||||
|
||||
return attrs.join('');
|
||||
}
|
||||
|
||||
function sanitizePreviewHtml(html) {
|
||||
var output = String(html || '');
|
||||
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
output = output.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||||
output = output.replace(/<[^>]+>/g, function (tag) {
|
||||
var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)([\s\S]*?)(\/?)>$/i);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var closing = Boolean(match[1]);
|
||||
var name = String(match[2] || '').toLowerCase();
|
||||
var attrText = String(match[3] || '');
|
||||
var allowed = ['a', 'b', 'blockquote', 'br', 'code', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'];
|
||||
if (allowed.indexOf(name) === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (closing) {
|
||||
return '</' + name + '>';
|
||||
}
|
||||
|
||||
return '<' + name + sanitizeTagAttributes(name, attrText) + '>';
|
||||
});
|
||||
|
||||
return output.replace(/<p\b([^>]*)>/gi, function (tag, attrText) {
|
||||
if (/\bstyle\s*=\s*/i.test(attrText)) {
|
||||
return tag.replace(/\bstyle\s*=\s*("([^"]*)"|'([^']*)')/i, function (full, quoted, doubleQuoted, singleQuoted) {
|
||||
var existingStyle = String(doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : '');
|
||||
if (/\bmargin\s*:/i.test(existingStyle)) {
|
||||
return full;
|
||||
}
|
||||
return 'style="margin:1em 0;' + existingStyle + '"';
|
||||
});
|
||||
}
|
||||
|
||||
return '<p' + attrText + ' style="margin:1em 0;">';
|
||||
});
|
||||
}
|
||||
|
||||
function renderPreviewListItem(item, tag) {
|
||||
if (item && typeof item === 'object') {
|
||||
var content = item.content !== undefined ? item.content : (item.text !== undefined ? item.text : item.value !== undefined ? item.value : '');
|
||||
var children = Array.isArray(item.items) ? item.items : Array.isArray(item.subItems) ? item.subItems : Array.isArray(item.children) ? item.children : [];
|
||||
var nested = children.length ? '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + children.map(function (child) { return renderPreviewListItem(child, tag); }).join('') + '</' + tag + '>' : '';
|
||||
return '<li>' + sanitizePreviewHtml(content || '') + nested + '</li>';
|
||||
}
|
||||
|
||||
return '<li>' + sanitizePreviewHtml(item || '') + '</li>';
|
||||
}
|
||||
|
||||
function renderPreviewTable(data) {
|
||||
var rows = Array.isArray(data.content) ? data.content : Array.isArray(data.rows) ? data.rows : [];
|
||||
if (!rows.length) {
|
||||
return '';
|
||||
}
|
||||
var hasHeadings = Boolean(data.withHeadings);
|
||||
var body = rows.map(function (row, rowIndex) {
|
||||
var cells = Array.isArray(row) ? row : [];
|
||||
var cellTag = hasHeadings && rowIndex === 0 ? 'th' : 'td';
|
||||
var cellAttrs = cellTag === 'th' ? ' scope="col"' : '';
|
||||
return '<tr>' + cells.map(function (cell) {
|
||||
return '<' + cellTag + cellAttrs + '>' + sanitizePreviewHtml(cell || '') + '</' + cellTag + '>';
|
||||
}).join('') + '</tr>';
|
||||
}).join('');
|
||||
return '<table class="slide-preview-table">' + body + '</table>';
|
||||
}
|
||||
|
||||
function renderPreviewBlock(block) {
|
||||
if (!block || !block.type || !block.data) {
|
||||
return '';
|
||||
}
|
||||
if (block.type === 'header') {
|
||||
var level = Math.max(1, Math.min(6, Number(block.data.level || 2)));
|
||||
return '<h' + level + '>' + sanitizePreviewHtml(block.data.text || '') + '</h' + level + '>';
|
||||
}
|
||||
if (block.type === 'list') {
|
||||
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
|
||||
var items = Array.isArray(block.data.items) ? block.data.items : [];
|
||||
return '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + items.map(function (item) { return renderPreviewListItem(item, tag); }).join('') + '</' + tag + '>';
|
||||
}
|
||||
if (block.type === 'delimiter') {
|
||||
return '<hr />';
|
||||
}
|
||||
if (block.type === 'code') {
|
||||
return '<pre><code>' + sanitizePreviewHtml(block.data.code || '') + '</code></pre>';
|
||||
}
|
||||
if (block.type === 'table') {
|
||||
return renderPreviewTable(block.data);
|
||||
}
|
||||
if (block.type === 'paragraph') {
|
||||
return '<p>' + sanitizePreviewHtml(block.data.text || '') + '</p>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderPreviewText(value) {
|
||||
var rawValue = value;
|
||||
if (rawValue && typeof rawValue === 'object') {
|
||||
if (rawValue.value !== undefined) {
|
||||
rawValue = rawValue.value;
|
||||
} else if (rawValue.text !== undefined) {
|
||||
rawValue = rawValue.text;
|
||||
} else if (rawValue.html !== undefined) {
|
||||
rawValue = rawValue.html;
|
||||
} else {
|
||||
try {
|
||||
rawValue = JSON.stringify(rawValue);
|
||||
} catch (_error) {
|
||||
rawValue = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
var raw = String(rawValue || '');
|
||||
if (!raw) {
|
||||
return '<div class="slide-preview-placeholder">Empty text</div>';
|
||||
}
|
||||
return sanitizePreviewHtml(raw);
|
||||
}
|
||||
|
||||
function renderPreviewTextRegion(region, value, style, scale) {
|
||||
var module = getRegionTypeModule('text');
|
||||
if (module && typeof module.renderPreview === 'function') {
|
||||
return module.renderPreview(region, value, style, scale);
|
||||
}
|
||||
|
||||
var fontFamily = style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '';
|
||||
var color = style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '';
|
||||
var fontSize = style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '';
|
||||
var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1));
|
||||
var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1));
|
||||
var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;' + fontFamily + fontSize + color;
|
||||
return '<div class="slide-preview-text-content" style="' + wrapperStyle + '">' + renderPreviewText(value) + '</div>';
|
||||
}
|
||||
|
||||
function startPreviewVideoPlayback(root) {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
var videos = root.querySelectorAll('video.slide-preview-video');
|
||||
Array.prototype.forEach.call(videos, function (video) {
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
video.autoplay = true;
|
||||
video.loop = true;
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
|
||||
function tryPlay() {
|
||||
try {
|
||||
video.load();
|
||||
} catch (_error) {
|
||||
// Ignore load failures and try to play anyway.
|
||||
}
|
||||
|
||||
var playPromise = video.play && video.play();
|
||||
if (playPromise && typeof playPromise.catch === 'function') {
|
||||
playPromise.catch(function () {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (video.readyState >= 2) {
|
||||
tryPlay();
|
||||
return;
|
||||
}
|
||||
|
||||
video.addEventListener('canplay', tryPlay, { once: true });
|
||||
video.addEventListener('loadedmetadata', tryPlay, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sanitizePreviewHtml: sanitizePreviewHtml,
|
||||
sanitizeTagAttributes: sanitizeTagAttributes,
|
||||
renderPreviewListItem: renderPreviewListItem,
|
||||
renderPreviewTable: renderPreviewTable,
|
||||
renderPreviewBlock: renderPreviewBlock,
|
||||
renderPreviewText: renderPreviewText,
|
||||
renderPreviewTextRegion: renderPreviewTextRegion,
|
||||
startPreviewVideoPlayback: startPreviewVideoPlayback
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
// Region-specific helpers for slide forms.
|
||||
|
||||
import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-preview.js';
|
||||
|
||||
export function createSlideFormRegionHelpers(options) {
|
||||
var settings = options || {};
|
||||
var templateFields = settings.templateFields || null;
|
||||
var existingContent = settings.existingContent || {};
|
||||
var rssFeeds = Array.isArray(settings.rssFeeds) ? settings.rssFeeds : [];
|
||||
var apiSources = Array.isArray(settings.apiSources) ? settings.apiSources : [];
|
||||
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () { return null; };
|
||||
var placeholderUtils = window.placeholderUtils || {};
|
||||
var placeholderChips = window.placeholderChips || {};
|
||||
var escapeHtml = typeof settings.escapeHtml === 'function'
|
||||
? settings.escapeHtml
|
||||
: function (value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
var previewHelpers = createSlideFormPreviewHelpers({
|
||||
getRegionTypeModule: getRegionTypeModule,
|
||||
escapeHtml: escapeHtml
|
||||
});
|
||||
var defaultFontSize = Math.max(1, Number(settings.defaultFontSize || 32));
|
||||
var uploadMaxLabel = String(settings.uploadMaxLabel || '100 MB');
|
||||
var uploadVideoMaxLabel = String(settings.uploadVideoMaxLabel || '1 GB');
|
||||
|
||||
function sanitizeFontSize(value) {
|
||||
var raw = String(value || '').trim().toLowerCase();
|
||||
if (/^\d+(?:\.\d+)?px$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw.replace(/px$/, '')))));
|
||||
}
|
||||
if (/^\d+(?:\.\d+)?$/.test(raw)) {
|
||||
return String(Math.max(1, Math.round(Number(raw))));
|
||||
}
|
||||
return String(defaultFontSize);
|
||||
}
|
||||
|
||||
var sanitizePreviewHtml = previewHelpers.sanitizePreviewHtml;
|
||||
var sanitizeTagAttributes = previewHelpers.sanitizeTagAttributes;
|
||||
var renderPreviewListItem = previewHelpers.renderPreviewListItem;
|
||||
var renderPreviewTable = previewHelpers.renderPreviewTable;
|
||||
var renderPreviewBlock = previewHelpers.renderPreviewBlock;
|
||||
var renderPreviewText = previewHelpers.renderPreviewText;
|
||||
var renderPreviewTextRegion = previewHelpers.renderPreviewTextRegion;
|
||||
var startPreviewVideoPlayback = previewHelpers.startPreviewVideoPlayback;
|
||||
|
||||
function getCurrentTextStyle(region) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var textRegion = getRegionTypeModule('text');
|
||||
var defaultStyle = window.pulseRegionTypes && typeof window.pulseRegionTypes.getDefaultRegionStyle === 'function'
|
||||
? window.pulseRegionTypes.getDefaultRegionStyle('text')
|
||||
: textRegion && typeof textRegion.getDefaultStyle === 'function'
|
||||
? textRegion.getDefaultStyle()
|
||||
: {
|
||||
font_family: 'Arial',
|
||||
font_size: defaultFontSize,
|
||||
font_color: '#000000'
|
||||
};
|
||||
return {
|
||||
font_family: String(current.font_family || region.font_family || defaultStyle.font_family || 'Arial').trim() || 'Arial',
|
||||
font_size: Math.max(8, Number(current.font_size || defaultStyle.font_size || defaultFontSize)),
|
||||
font_color: String(current.font_color || region.font_color || defaultStyle.font_color || '#000000').trim() || '#000000'
|
||||
};
|
||||
}
|
||||
|
||||
function getRssFeedById(feedId) {
|
||||
var normalizedId = Number(feedId || 0);
|
||||
return rssFeeds.find(function (feed) {
|
||||
return Number(feed.id) === normalizedId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getRssFieldList(feedId) {
|
||||
var module = getRegionTypeModule('rss');
|
||||
if (module && typeof module.getFieldList === 'function') {
|
||||
return module.getFieldList(feedId, rssFeeds);
|
||||
}
|
||||
|
||||
var feed = getRssFeedById(feedId);
|
||||
var sampleItem = feed && Array.isArray(feed.items) ? feed.items[0] : null;
|
||||
var sampleJson = sampleItem && sampleItem.itemJson && typeof sampleItem.itemJson === 'object' ? sampleItem.itemJson : null;
|
||||
|
||||
function walkFields(value, prefix, output) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(value).forEach(function (key) {
|
||||
if (key === 'rawXml') {
|
||||
return;
|
||||
}
|
||||
|
||||
var nextPath = prefix ? prefix + '.' + key : key;
|
||||
var nextValue = value[key];
|
||||
if (nextValue && typeof nextValue === 'object' && !Array.isArray(nextValue)) {
|
||||
walkFields(nextValue, nextPath, output);
|
||||
return;
|
||||
}
|
||||
|
||||
if (output.indexOf(nextPath) === -1) {
|
||||
output.push(nextPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var fields = [];
|
||||
walkFields(sampleJson, '', fields);
|
||||
return fields;
|
||||
}
|
||||
|
||||
function buildLimitedPlaceholderChipMarkup(fieldList, emptyLabel) {
|
||||
var fields = Array.isArray(fieldList) ? fieldList : [];
|
||||
var visibleFields = fields.slice(0, 8);
|
||||
var overflowFields = fields.slice(visibleFields.length);
|
||||
|
||||
if (!fields.length) {
|
||||
return '<span class="muted slide-image-file">' + escapeHtml(emptyLabel) + '</span>';
|
||||
}
|
||||
|
||||
var visibleMarkup = visibleFields.map(function (field) {
|
||||
return typeof placeholderChips.renderChip === 'function'
|
||||
? placeholderChips.renderChip(field)
|
||||
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('');
|
||||
|
||||
if (!overflowFields.length) {
|
||||
return visibleMarkup;
|
||||
}
|
||||
|
||||
return '' +
|
||||
visibleMarkup +
|
||||
'<details class="w-100 mt-2" data-placeholder-chips-more>' +
|
||||
'<summary class="small text-body-secondary">Show ' + escapeHtml(overflowFields.length) + ' more fields</summary>' +
|
||||
'<div class="d-flex flex-wrap gap-2 mt-2">' + overflowFields.map(function (field) {
|
||||
return typeof placeholderChips.renderChip === 'function'
|
||||
? placeholderChips.renderChip(field)
|
||||
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('') + '</div>' +
|
||||
'</details>';
|
||||
}
|
||||
|
||||
function truncateJsonPreview(value, maxLength) {
|
||||
var text = '';
|
||||
try {
|
||||
text = JSON.stringify(value, null, 2);
|
||||
} catch (_error) {
|
||||
text = String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var limit = Math.max(200, Number(maxLength || 1100));
|
||||
if (text.length <= limit) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0, limit).replace(/\s+$/, '') + '\n...';
|
||||
}
|
||||
|
||||
function buildApiSampleDataMarkup(sourceId, itemNumber, itemsPathOverride) {
|
||||
var source = getApiSourceById(sourceId);
|
||||
if (!source) {
|
||||
return '<div class="api-region-sample-meta">Select an API source to preview sample data.</div>';
|
||||
}
|
||||
|
||||
var item = getApiSourceItems(sourceId, itemsPathOverride)[Math.max(0, Math.max(1, Number(itemNumber || 1)) - 1)] || null;
|
||||
if (!item) {
|
||||
return '<div class="api-region-sample-meta">No sample item is available for the current selection.</div>';
|
||||
}
|
||||
|
||||
return '<pre class="api-region-sample-preview" data-api-sample-data-output>' + escapeHtml(truncateJsonPreview(item, 1100)) + '</pre>';
|
||||
}
|
||||
|
||||
function updatePlaceholderChipList(card, fieldList, emptyLabel) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = card.querySelector('[data-placeholder-chips]');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chips = Array.isArray(fieldList) ? fieldList.map(function (field) {
|
||||
return typeof placeholderChips.renderChip === 'function'
|
||||
? placeholderChips.renderChip(field)
|
||||
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('') : '';
|
||||
|
||||
container.innerHTML = chips || '<span class="muted slide-image-file">' + escapeHtml(emptyLabel) + '</span>';
|
||||
}
|
||||
|
||||
function updateRssPlaceholderChips(regionId, feedId) {
|
||||
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
updatePlaceholderChipList(card, getRssFieldList(feedId), 'No RSS fields available.');
|
||||
}
|
||||
|
||||
function getCurrentRssConfig(region) {
|
||||
var module = getRegionTypeModule('rss');
|
||||
if (module && typeof module.getCurrentConfig === 'function') {
|
||||
return module.getCurrentConfig(region, existingContent);
|
||||
}
|
||||
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var parsedItemNumber = Math.max(1, Number(current.item_number || 1));
|
||||
return {
|
||||
feed_id: current.feed_id === undefined || current.feed_id === null || current.feed_id === '' ? '' : Number(current.feed_id),
|
||||
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
||||
value: current.value !== undefined ? current.value : ''
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePath(value, path) {
|
||||
var current = value;
|
||||
if (!path) {
|
||||
return current;
|
||||
}
|
||||
String(path).split('.').forEach(function (segment) {
|
||||
if (current === undefined || current === null) {
|
||||
current = '';
|
||||
return;
|
||||
}
|
||||
current = current[segment];
|
||||
});
|
||||
return current === undefined || current === null ? '' : current;
|
||||
}
|
||||
|
||||
function substituteRssVariables(html, item) {
|
||||
var source = String(html || '');
|
||||
return source.replace(/\{\{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_.]+))?\s*\}\}/g, function (_match, tokenName, tokenPath) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return '';
|
||||
}
|
||||
var key = tokenName === 'item' && tokenPath ? tokenPath : tokenName;
|
||||
return escapeHtml(resolvePath(item, key || ''));
|
||||
});
|
||||
}
|
||||
|
||||
function getRssItem(feedId, itemNumber) {
|
||||
var module = getRegionTypeModule('rss');
|
||||
if (module && typeof module.getItem === 'function') {
|
||||
return module.getItem(feedId, itemNumber, rssFeeds);
|
||||
}
|
||||
|
||||
var feed = getRssFeedById(feedId);
|
||||
var items = feed && Array.isArray(feed.items) ? feed.items : [];
|
||||
var parsedItemNumber = Math.max(1, Number(itemNumber || 1));
|
||||
var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0;
|
||||
return items[index] || null;
|
||||
}
|
||||
|
||||
function getRssPreviewFallback(item) {
|
||||
var module = getRegionTypeModule('rss');
|
||||
if (module && typeof module.getPreviewFallback === 'function') {
|
||||
return module.getPreviewFallback(item);
|
||||
}
|
||||
|
||||
if (!item || typeof item !== 'object') {
|
||||
return '<div class="slide-preview-placeholder">RSS item</div>';
|
||||
}
|
||||
|
||||
var title = String(item.title || '').trim();
|
||||
var description = String(item.description || '').trim();
|
||||
var summary = [];
|
||||
if (title) {
|
||||
summary.push('<h3>' + escapeHtml(title) + '</h3>');
|
||||
}
|
||||
if (description) {
|
||||
summary.push('<p>' + sanitizePreviewHtml(description) + '</p>');
|
||||
}
|
||||
if (!summary.length) {
|
||||
return '<div class="slide-preview-placeholder">RSS item</div>';
|
||||
}
|
||||
return summary.join('');
|
||||
}
|
||||
|
||||
function getApiSourceById(sourceId) {
|
||||
var normalizedId = Number(sourceId || 0);
|
||||
return apiSources.find(function (source) {
|
||||
return Number(source.id) === normalizedId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getApiSourceItemsPath(source, overridePath) {
|
||||
if (overridePath === undefined || overridePath === null) {
|
||||
return String(source && (source.items_path || source.itemsPath) || '').trim();
|
||||
}
|
||||
|
||||
return String(overridePath || '').trim();
|
||||
}
|
||||
|
||||
function getApiSourceItems(sourceId, itemsPathOverride) {
|
||||
var source = getApiSourceById(sourceId);
|
||||
var module = getRegionTypeModule('api');
|
||||
if (module && typeof module.getItems === 'function') {
|
||||
return module.getItems(sourceId, apiSources, itemsPathOverride);
|
||||
}
|
||||
|
||||
var responseJson = source && source.responseJson && typeof source.responseJson === 'object' ? source.responseJson : null;
|
||||
var itemsPath = getApiSourceItemsPath(source, itemsPathOverride);
|
||||
if (itemsPath !== undefined && itemsPath !== null && itemsPath !== '') {
|
||||
var current = responseJson;
|
||||
String(itemsPath).split('.').forEach(function (segment) {
|
||||
if (current === undefined || current === null) {
|
||||
current = '';
|
||||
return;
|
||||
}
|
||||
current = current[segment];
|
||||
});
|
||||
return Array.isArray(current) ? current : [];
|
||||
}
|
||||
if (Array.isArray(responseJson)) {
|
||||
return responseJson;
|
||||
}
|
||||
if (responseJson && Array.isArray(responseJson.items)) {
|
||||
return responseJson.items;
|
||||
}
|
||||
if (responseJson && Array.isArray(responseJson.results)) {
|
||||
return responseJson.results;
|
||||
}
|
||||
if (responseJson && Array.isArray(responseJson.data)) {
|
||||
return responseJson.data;
|
||||
}
|
||||
return responseJson ? [responseJson] : [];
|
||||
}
|
||||
|
||||
function getApiItemCount(sourceId, itemsPathOverride) {
|
||||
var module = getRegionTypeModule('api');
|
||||
if (module && typeof module.getItemCount === 'function') {
|
||||
return module.getItemCount(sourceId, apiSources, itemsPathOverride);
|
||||
}
|
||||
|
||||
return getApiSourceItems(sourceId, itemsPathOverride).length;
|
||||
}
|
||||
|
||||
function getApiFieldList(sourceId, itemsPathOverride) {
|
||||
var sampleItem = getApiSourceItems(sourceId, itemsPathOverride)[0] || null;
|
||||
if (typeof placeholderUtils.collectPlaceholderFieldPaths === 'function') {
|
||||
return placeholderUtils.collectPlaceholderFieldPaths(sampleItem && typeof sampleItem === 'object' ? sampleItem : null);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function updateApiPlaceholderChips(regionId, sourceId, itemsPathOverride) {
|
||||
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = card.querySelector('[data-placeholder-chips]');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = buildLimitedPlaceholderChipMarkup(getApiFieldList(sourceId, itemsPathOverride), 'No JSON fields available.');
|
||||
}
|
||||
|
||||
function updateApiSampleDataPanel(regionId, sourceId, itemNumber, itemsPathOverride) {
|
||||
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
var container = card.querySelector('[data-api-sample-data-panel]');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = buildApiSampleDataMarkup(sourceId, itemNumber, itemsPathOverride);
|
||||
}
|
||||
|
||||
function updateApiItemNumberLimit(regionId, sourceId, itemsPathOverride) {
|
||||
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
var input = card.querySelector('input[name="region_api_item_number_' + regionId + '"]');
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
var itemCount = Math.max(1, Number(getApiItemCount(sourceId, itemsPathOverride) || 1));
|
||||
input.setAttribute('max', String(itemCount));
|
||||
if (Number(input.value || 0) > itemCount) {
|
||||
input.value = String(itemCount);
|
||||
}
|
||||
}
|
||||
|
||||
function clampApiItemNumber(regionId, sourceId, itemNumber, itemsPathOverride) {
|
||||
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
||||
if (!card) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var input = card.querySelector('input[name="region_api_item_number_' + regionId + '"]');
|
||||
if (!input) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var itemCount = Math.max(1, Number(getApiItemCount(sourceId, itemsPathOverride) || 1));
|
||||
var parsedValue = Math.max(1, Number(itemNumber || 1));
|
||||
var clampedValue = Math.min(itemCount, Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : 1);
|
||||
input.setAttribute('max', String(itemCount));
|
||||
input.value = String(clampedValue);
|
||||
return String(clampedValue);
|
||||
}
|
||||
|
||||
function getCurrentApiConfig(region) {
|
||||
var module = getRegionTypeModule('api');
|
||||
if (module && typeof module.getCurrentConfig === 'function') {
|
||||
return module.getCurrentConfig(region, existingContent, apiSources);
|
||||
}
|
||||
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var source = getApiSourceById(current.source_id);
|
||||
var parsedItemNumber = Math.max(1, Number(current.item_number || 1));
|
||||
return {
|
||||
source_id: current.source_id === undefined || current.source_id === null || current.source_id === '' ? '' : Number(current.source_id),
|
||||
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
||||
items_path: Object.prototype.hasOwnProperty.call(current, 'items_path') ? String(current.items_path === undefined || current.items_path === null ? '' : current.items_path) : undefined,
|
||||
default_items_path: String(source && (source.items_path || source.itemsPath) || '').trim(),
|
||||
value: current.value !== undefined ? current.value : ''
|
||||
};
|
||||
}
|
||||
|
||||
function reduceAspectRatio(width, height) {
|
||||
var a = Math.max(1, Math.round(Number(width || 0) || 1));
|
||||
var b = Math.max(1, Math.round(Number(height || 0) || 1));
|
||||
var x = a;
|
||||
var y = b;
|
||||
|
||||
while (y !== 0) {
|
||||
var remainder = x % y;
|
||||
x = y;
|
||||
y = remainder;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.round(a / x)) + ':' + Math.max(1, Math.round(b / x));
|
||||
}
|
||||
|
||||
function getCurrentRegionValue(region) {
|
||||
var current = existingContent[region.region_key];
|
||||
return current && current.value !== undefined ? current.value : '';
|
||||
}
|
||||
|
||||
function getCurrentTimeDateConfig(region) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
return {
|
||||
value: current.value !== undefined ? current.value : '',
|
||||
timezone: current.timezone !== undefined ? current.timezone : (current.time_zone !== undefined ? current.time_zone : '')
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentRegionVideoDuration(region) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var duration = Math.round(Number(current.duration_seconds || 0) * 1000) / 1000;
|
||||
return Number.isFinite(duration) && duration > 0 ? String(duration) : '';
|
||||
}
|
||||
|
||||
function buildRegionEditorCardContext(region) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (module && typeof module.buildEditorCardContext === 'function') {
|
||||
return module.buildEditorCardContext({
|
||||
region: region,
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
||||
currentDuration: getCurrentRegionVideoDuration(region),
|
||||
regionRatio: reduceAspectRatio(region.width, region.height),
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
||||
disableAudio: (existingContent[region.region_key] || {}).disable_audio,
|
||||
config: region.region_type === 'api' ? getCurrentApiConfig(region) : getCurrentRssConfig(region),
|
||||
style: getCurrentTextStyle(region),
|
||||
fontSize: getCurrentTextStyle(region).font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
var selected = Number(feed.id) === Number(getCurrentRssConfig(region).feed_id) ? ' selected' : '';
|
||||
return '<option value="' + escapeHtml(feed.id) + '"' + selected + '>' + escapeHtml(feed.name || ('Feed ' + feed.id)) + '</option>';
|
||||
}).join(''),
|
||||
sourceOptions: apiSources.map(function (source) {
|
||||
var selected = Number(source.id) === Number(getCurrentApiConfig(region).source_id) ? ' selected' : '';
|
||||
return '<option value="' + escapeHtml(source.id) + '"' + selected + '>' + escapeHtml(source.name || ('Source ' + source.id)) + '</option>';
|
||||
}).join(''),
|
||||
placeholderChips: region.region_type === 'rss'
|
||||
? getRssFieldList(getCurrentRssConfig(region).feed_id).map(function (field) {
|
||||
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('')
|
||||
: buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.'),
|
||||
sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path)
|
||||
}, existingContent, region.region_type === 'rss' ? rssFeeds : apiSources);
|
||||
}
|
||||
|
||||
var textStyle = getCurrentTextStyle(region);
|
||||
var rssConfig = getCurrentRssConfig(region);
|
||||
var apiConfig = getCurrentApiConfig(region);
|
||||
var currentContent = existingContent[region.region_key] || {};
|
||||
var apiItemsPath = apiConfig.items_path !== undefined ? apiConfig.items_path : apiConfig.default_items_path;
|
||||
|
||||
return {
|
||||
region: region,
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' ? currentContent : getCurrentRegionValue(region),
|
||||
currentDuration: getCurrentRegionVideoDuration(region),
|
||||
regionRatio: reduceAspectRatio(region.width, region.height),
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
||||
disableAudio: currentContent.disable_audio,
|
||||
config: region.region_type === 'api' ? apiConfig : rssConfig,
|
||||
style: textStyle,
|
||||
fontSize: textStyle.font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
var selected = Number(feed.id) === Number(rssConfig.feed_id) ? ' selected' : '';
|
||||
return '<option value="' + escapeHtml(feed.id) + '"' + selected + '>' + escapeHtml(feed.name || ('Feed ' + feed.id)) + '</option>';
|
||||
}).join(''),
|
||||
sourceOptions: apiSources.map(function (source) {
|
||||
var selected = Number(source.id) === Number(apiConfig.source_id) ? ' selected' : '';
|
||||
return '<option value="' + escapeHtml(source.id) + '"' + selected + '>' + escapeHtml(source.name || ('Source ' + source.id)) + '</option>';
|
||||
}).join(''),
|
||||
placeholderChips: region.region_type === 'rss'
|
||||
? getRssFieldList(rssConfig.feed_id).map(function (field) {
|
||||
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('')
|
||||
: buildLimitedPlaceholderChipMarkup(getApiFieldList(apiConfig.source_id, apiItemsPath), 'No JSON fields available.'),
|
||||
sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath)
|
||||
};
|
||||
}
|
||||
|
||||
function getRegionEditorCardHtml(region) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (!module || typeof module.renderEditorCard !== 'function') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return module.renderEditorCard(buildRegionEditorCardContext(region));
|
||||
}
|
||||
|
||||
function getPreviewRegionContent(card, region) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : apiSources);
|
||||
}
|
||||
|
||||
var current = existingContent[region.region_key];
|
||||
var content = current && typeof current === 'object' ? Object.assign({}, current) : {};
|
||||
if (!card) {
|
||||
return content;
|
||||
}
|
||||
|
||||
var hiddenInput = card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]');
|
||||
var textAreaInput = card.querySelector('textarea[name="region_text_' + region.id + '"]');
|
||||
var htmlInput = card.querySelector('textarea[name="region_html_' + region.id + '"]');
|
||||
var timeDateTimezoneInput = card.querySelector('input[name="region_timezone_' + region.id + '"]');
|
||||
var imageHidden = card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]');
|
||||
var imageFileInput = card.querySelector('input[type="file"][name="region_image_' + region.id + '"]');
|
||||
var videoHidden = card.querySelector('input[type="hidden"][name="existing_region_video_' + region.id + '"]');
|
||||
var videoFileInput = card.querySelector('input[type="file"][name="region_video_' + region.id + '"]');
|
||||
var webpageInput = card.querySelector('input[type="url"][name="region_webpage_' + region.id + '"]');
|
||||
var rtmpInput = card.querySelector('input[type="url"][name="region_rtmp_' + region.id + '"]');
|
||||
var disableAudioInput = card.querySelector('input[type="checkbox"][name="region_disable_audio_' + region.id + '"]');
|
||||
var rssFeedInput = card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]');
|
||||
var rssItemInput = card.querySelector('input[name="region_rss_item_number_' + region.id + '"]');
|
||||
var apiSourceInput = card.querySelector('select[name="region_api_source_id_' + region.id + '"]');
|
||||
var apiItemInput = card.querySelector('input[name="region_api_item_number_' + region.id + '"]');
|
||||
var apiItemsPathInput = card.querySelector('input[name="region_api_items_path_' + region.id + '"]');
|
||||
|
||||
if (region.region_type === 'image') {
|
||||
content.value = (imageFileInput && imageFileInput.dataset.previewUrl) ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : content.value || '');
|
||||
} else if (region.region_type === 'video') {
|
||||
content.value = (videoFileInput && videoFileInput.dataset.previewUrl) ? videoFileInput.dataset.previewUrl : (videoHidden ? videoHidden.value : content.value || '');
|
||||
} else if (region.region_type === 'webpage') {
|
||||
content.value = webpageInput ? webpageInput.value : (content.value || '');
|
||||
} else if (region.region_type === 'rtmp') {
|
||||
content.value = rtmpInput ? rtmpInput.value : (content.value || '');
|
||||
content.disable_audio = disableAudioInput ? disableAudioInput.checked : (content.disable_audio === undefined ? true : Boolean(content.disable_audio));
|
||||
} else if (region.region_type === 'html') {
|
||||
content.value = htmlInput ? htmlInput.value : (content.value || '');
|
||||
} else if (region.region_type === 'time-date') {
|
||||
var timeDateCurrent = getCurrentTimeDateConfig(region);
|
||||
content.value = textAreaInput ? textAreaInput.value : (timeDateCurrent.value || '');
|
||||
content.timezone = timeDateTimezoneInput ? timeDateTimezoneInput.value : (timeDateCurrent.timezone || '');
|
||||
} else if (region.region_type === 'rss') {
|
||||
content.value = hiddenInput ? hiddenInput.value : (content.value || '');
|
||||
content.feed_id = rssFeedInput ? rssFeedInput.value : content.feed_id;
|
||||
content.item_number = rssItemInput ? rssItemInput.value : content.item_number;
|
||||
} else if (region.region_type === 'api') {
|
||||
content.value = hiddenInput ? hiddenInput.value : (content.value || '');
|
||||
content.source_id = apiSourceInput ? apiSourceInput.value : content.source_id;
|
||||
content.item_number = apiItemInput ? apiItemInput.value : content.item_number;
|
||||
content.items_path = apiItemsPathInput ? apiItemsPathInput.value : content.items_path;
|
||||
} else {
|
||||
content.value = hiddenInput ? hiddenInput.value : (content.value || '');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : apiSources);
|
||||
}
|
||||
|
||||
var textStyle = getCurrentTextStyle(region);
|
||||
var previewContent = getPreviewRegionContent(card, region);
|
||||
return {
|
||||
value: previewContent.value,
|
||||
timezone: previewContent.timezone,
|
||||
style: textStyle,
|
||||
existingContent: existingContent,
|
||||
feeds: rssFeeds,
|
||||
sources: apiSources,
|
||||
disable_audio: previewContent.disable_audio === undefined ? true : Boolean(previewContent.disable_audio),
|
||||
feed_id: previewContent.feed_id,
|
||||
item_number: previewContent.item_number,
|
||||
source_id: previewContent.source_id,
|
||||
items_path: previewContent.items_path,
|
||||
font_size: textStyle.font_size,
|
||||
font_color: textStyle.font_color,
|
||||
font_family: textStyle.font_family
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sanitizePreviewHtml: sanitizePreviewHtml,
|
||||
sanitizeTagAttributes: sanitizeTagAttributes,
|
||||
getCurrentTextStyle: getCurrentTextStyle,
|
||||
getRssFieldList: getRssFieldList,
|
||||
updateRssPlaceholderChips: updateRssPlaceholderChips,
|
||||
getCurrentRssConfig: getCurrentRssConfig,
|
||||
getApiFieldList: getApiFieldList,
|
||||
updateApiPlaceholderChips: updateApiPlaceholderChips,
|
||||
updateApiSampleDataPanel: updateApiSampleDataPanel,
|
||||
updateApiItemNumberLimit: updateApiItemNumberLimit,
|
||||
getCurrentApiConfig: getCurrentApiConfig,
|
||||
reduceAspectRatio: reduceAspectRatio,
|
||||
renderPreviewTextRegion: renderPreviewTextRegion,
|
||||
startPreviewVideoPlayback: startPreviewVideoPlayback,
|
||||
getRegionEditorCardHtml: getRegionEditorCardHtml,
|
||||
buildRegionEditorCardContext: buildRegionEditorCardContext,
|
||||
getPreviewRegionContent: getPreviewRegionContent,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext,
|
||||
getCurrentRegionValue: getCurrentRegionValue,
|
||||
getCurrentRegionVideoDuration: getCurrentRegionVideoDuration
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Template selection lock controller for slide forms.
|
||||
|
||||
export function createTemplateSelectorLockController(templateSelect) {
|
||||
var locked = false;
|
||||
var armed = false;
|
||||
|
||||
+264
-1787
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
// Slide image cropper modal wiring and file replacement helpers.
|
||||
|
||||
(function () {
|
||||
var templateFields = document.getElementById('template-fields');
|
||||
var modal = document.getElementById('slide-image-cropper-modal');
|
||||
|
||||
@@ -15,6 +15,35 @@
|
||||
if (typeof window.initLocalDateTimes === 'function') {
|
||||
window.initLocalDateTimes(container);
|
||||
}
|
||||
|
||||
syncTablePaginationCards(container);
|
||||
syncTablePaginationPageClass();
|
||||
}
|
||||
|
||||
function syncTablePaginationCards(root) {
|
||||
if (!root || !root.querySelectorAll) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(root.querySelectorAll('[data-table-pagination-card]'), function (card) {
|
||||
if (!card || !card.getBoundingClientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cardRect = card.getBoundingClientRect();
|
||||
var bottomInset = 16;
|
||||
var availableHeight = Math.max(0, window.innerHeight - cardRect.top - bottomInset);
|
||||
card.style.removeProperty('--table-pagination-card-height');
|
||||
card.style.setProperty('--table-pagination-card-max-height', availableHeight + 'px');
|
||||
});
|
||||
}
|
||||
|
||||
function syncTablePaginationPageClass() {
|
||||
if (!document || !document.body || !document.body.classList) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.classList.toggle('table-pagination-page', !!document.querySelector('[data-table-pagination-card]'));
|
||||
}
|
||||
|
||||
function focusSearchInput(input) {
|
||||
@@ -163,8 +192,21 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
syncTablePaginationCards(scope);
|
||||
}
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('resize', function () {
|
||||
syncTablePaginationCards(document);
|
||||
});
|
||||
window.addEventListener('orientationchange', function () {
|
||||
syncTablePaginationCards(document);
|
||||
});
|
||||
}
|
||||
|
||||
window.initTableSearches = initTableSearches;
|
||||
window.replaceTableResults = replaceTableResults;
|
||||
syncTablePaginationCards(document);
|
||||
syncTablePaginationPageClass();
|
||||
}());
|
||||
@@ -1,3 +1,5 @@
|
||||
// Table sorting helpers for clickable list headers.
|
||||
|
||||
(function () {
|
||||
function getTableSortState(table) {
|
||||
if (!table._tableSortState) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Shared template designer utility helpers.
|
||||
|
||||
(function () {
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
@@ -43,8 +45,12 @@
|
||||
}
|
||||
|
||||
function getDefaultRegionSize(regionType, lockRatio) {
|
||||
if (window.pulseRegionTypes && typeof window.pulseRegionTypes.getDefaultRegionSize === 'function') {
|
||||
return window.pulseRegionTypes.getDefaultRegionSize(regionType, lockRatio);
|
||||
}
|
||||
|
||||
var ratio = parseLockRatio(lockRatio);
|
||||
var locked = regionType === 'image' || regionType === 'video' || regionType === 'webpage' || regionType === 'html' || regionType === 'rtmp' || regionType === 'rss' || regionType === 'api';
|
||||
var locked = false;
|
||||
|
||||
if (ratio) {
|
||||
if (ratio.ratio >= 1) {
|
||||
@@ -89,7 +95,6 @@
|
||||
region_key: name,
|
||||
label: name,
|
||||
region_type: card.querySelector('[name="region_type[]"]').value,
|
||||
font_family: card.querySelector('[name="font_family[]"]').value,
|
||||
lock_ratio: normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value),
|
||||
x: Number(card.querySelector('[name="region_x[]"]').value || 0),
|
||||
y: Number(card.querySelector('[name="region_y[]"]').value || 0),
|
||||
@@ -108,7 +113,6 @@
|
||||
syncRegionIdentity(card, values.label);
|
||||
}
|
||||
if (values.region_type !== undefined) { card.querySelector('[name="region_type[]"]').value = values.region_type; }
|
||||
if (values.font_family !== undefined) { card.querySelector('[name="font_family[]"]').value = values.font_family; }
|
||||
if (values.lock_ratio !== undefined) { card.querySelector('[name="region_lock_ratio[]"]').value = normalizeLockRatio(values.lock_ratio); }
|
||||
if (values.x !== undefined) { card.querySelector('[name="region_x[]"]').value = Math.round(values.x); }
|
||||
if (values.y !== undefined) { card.querySelector('[name="region_y[]"]').value = Math.round(values.y); }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Template designer bootstrap and form wiring.
|
||||
|
||||
(function () {
|
||||
var dataElement = document.getElementById('template-editor-data');
|
||||
if (!dataElement) {
|
||||
@@ -33,6 +35,7 @@
|
||||
var removeBackgroundFlag = document.getElementById('remove-background-image-flag');
|
||||
var addRegionButton = document.getElementById('add-region-button');
|
||||
var regionAddModal = document.getElementById('region-add-modal');
|
||||
var regionAddOptions = document.getElementById('region-add-options');
|
||||
var regionCardTemplate = document.getElementById('region-card-template');
|
||||
var regionsJsonInput = document.getElementById('regions-json');
|
||||
var templateForm = document.getElementById('template-form');
|
||||
@@ -101,7 +104,7 @@
|
||||
return utils.getDefaultRegionSize(regionType, lockRatio);
|
||||
}
|
||||
var ratio = parseLockRatio(lockRatio);
|
||||
var locked = regionType === 'image' || regionType === 'webpage' || regionType === 'html' || regionType === 'rtmp' || regionType === 'rss';
|
||||
var locked = false;
|
||||
if (ratio) {
|
||||
if (ratio.ratio >= 1) {
|
||||
var baseWidth = locked ? 420 : 300;
|
||||
@@ -113,6 +116,41 @@
|
||||
return { width: locked ? 420 : 300, height: locked ? 240 : 120 };
|
||||
}
|
||||
|
||||
function getRegionTypeEntries() {
|
||||
if (window.pulseRegionTypes && typeof window.pulseRegionTypes.list === 'function') {
|
||||
return window.pulseRegionTypes.list();
|
||||
}
|
||||
|
||||
return ['api', 'html', 'image', 'rss', 'rtmp', 'text', 'time-date', 'video', 'webpage'].map(function (type) {
|
||||
return { type: type, definition: window.pulseRegionTypes && typeof window.pulseRegionTypes.get === 'function' ? window.pulseRegionTypes.get(type) || {} : {} };
|
||||
});
|
||||
}
|
||||
|
||||
function formatRegionTypeLabel(regionType) {
|
||||
return String(regionType || '')
|
||||
.trim()
|
||||
.replace(/(^|[_-])(\w)/g, function (_match, _prefix, letter) {
|
||||
return String(letter || '').toUpperCase();
|
||||
}) || 'Region';
|
||||
}
|
||||
|
||||
function renderAddRegionOptions() {
|
||||
if (!regionAddOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
regionAddOptions.innerHTML = getRegionTypeEntries().map(function (entry) {
|
||||
var type = String(entry && entry.type ? entry.type : '').trim();
|
||||
if (!type) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var definition = entry.definition || {};
|
||||
var label = definition.label || formatRegionTypeLabel(type);
|
||||
return '<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="' + escapeHtml(type) + '">' + escapeHtml(label) + '</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getCards() {
|
||||
return Array.prototype.slice.call(regionList.querySelectorAll('.region-item'));
|
||||
}
|
||||
@@ -179,7 +217,6 @@
|
||||
region_key: getRegionName(card),
|
||||
label: getRegionName(card),
|
||||
region_type: card.querySelector('[name="region_type[]"]').value,
|
||||
font_family: card.querySelector('[name="font_family[]"]').value,
|
||||
lock_ratio: normalizeLockRatio(card.querySelector('[name="region_lock_ratio[]"]').value),
|
||||
x: Number(card.querySelector('[name="region_x[]"]').value || 0),
|
||||
y: Number(card.querySelector('[name="region_y[]"]').value || 0),
|
||||
@@ -202,7 +239,6 @@
|
||||
syncRegionIdentity(card, values.label);
|
||||
}
|
||||
if (values.region_type !== undefined) { card.querySelector('[name="region_type[]"]').value = values.region_type; }
|
||||
if (values.font_family !== undefined) { card.querySelector('[name="font_family[]"]').value = values.font_family; }
|
||||
if (values.lock_ratio !== undefined) { card.querySelector('[name="region_lock_ratio[]"]').value = normalizeLockRatio(values.lock_ratio); }
|
||||
if (values.x !== undefined) { card.querySelector('[name="region_x[]"]').value = Math.round(values.x); }
|
||||
if (values.y !== undefined) { card.querySelector('[name="region_y[]"]').value = Math.round(values.y); }
|
||||
@@ -328,6 +364,10 @@
|
||||
}
|
||||
|
||||
function getRegionChipLabel(regionType) {
|
||||
if (window.pulseRegionTypes && typeof window.pulseRegionTypes.getRegionChipLabel === 'function') {
|
||||
return window.pulseRegionTypes.getRegionChipLabel(regionType);
|
||||
}
|
||||
|
||||
return regionType === 'image' ? 'Image' : regionType === 'video' ? 'Video' : regionType === 'webpage' ? 'Webpage' : regionType === 'html' ? 'HTML' : regionType === 'rtmp' ? 'RTMP' : regionType === 'rss' ? 'RSS' : 'Text';
|
||||
}
|
||||
|
||||
@@ -335,7 +375,6 @@
|
||||
var chip = card.querySelector('[data-region-chip]');
|
||||
var title = card.querySelector('[data-region-title]');
|
||||
var nameInput = card.querySelector('[name="region_name[]"]');
|
||||
var fontFamilyInput = card.querySelector('[name="font_family[]"]');
|
||||
var regionTypeInput = card.querySelector('[name="region_type[]"]');
|
||||
var regionKeyInput = card.querySelector('[name="region_key[]"]');
|
||||
var regionLabelInput = card.querySelector('[name="region_label[]"]');
|
||||
@@ -349,9 +388,6 @@
|
||||
if (nameInput) {
|
||||
nameInput.value = region.region_key || region.label || '';
|
||||
}
|
||||
if (fontFamilyInput) {
|
||||
fontFamilyInput.value = region.region_type === 'image' || region.region_type === 'video' || region.region_type === 'rtmp' ? '' : (region.font_family || 'Arial');
|
||||
}
|
||||
if (regionTypeInput) {
|
||||
regionTypeInput.value = region.region_type || 'text';
|
||||
}
|
||||
@@ -577,7 +613,6 @@
|
||||
region_key: name,
|
||||
label: name,
|
||||
region_type: type,
|
||||
font_family: type === 'text' || type === 'html' || type === 'rss' || type === 'api' ? 'Arial' : '',
|
||||
x: 80,
|
||||
y: 80,
|
||||
width: size.width,
|
||||
@@ -753,19 +788,26 @@
|
||||
}
|
||||
|
||||
if (addRegionButton && regionAddModal) {
|
||||
var addRegionTypeButtons = regionAddModal.querySelectorAll('[data-add-region-type]');
|
||||
addRegionButton.addEventListener('click', function () {
|
||||
renderAddRegionOptions();
|
||||
openAddRegionModal();
|
||||
});
|
||||
Array.prototype.forEach.call(addRegionTypeButtons, function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
if (regionAddOptions) {
|
||||
regionAddOptions.addEventListener('click', function (event) {
|
||||
var button = event.target && event.target.closest ? event.target.closest('[data-add-region-type]') : null;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
var regionType = button.getAttribute('data-add-region-type');
|
||||
addRegion(createDefaultRegion(regionType));
|
||||
if (window.pulseModal) {
|
||||
window.pulseModal.hide(regionAddModal);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderAddRegionOptions();
|
||||
}
|
||||
backgroundInput.addEventListener('change', function () {
|
||||
var file = backgroundInput.files && backgroundInput.files[0];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Theme bootstrap script that applies the saved or preferred color scheme.
|
||||
|
||||
(function () {
|
||||
var storageKey = 'lte-theme';
|
||||
var theme = 'auto';
|
||||
var ckeditorThemeStyleId = 'ckeditor-dark-theme-overrides';
|
||||
|
||||
function getPreferredTheme() {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
@@ -11,57 +12,6 @@
|
||||
return 'light';
|
||||
}
|
||||
|
||||
function getOrCreateCkeditorThemeStyleElement() {
|
||||
var styleElement = document.getElementById(ckeditorThemeStyleId);
|
||||
|
||||
if (styleElement) {
|
||||
return styleElement;
|
||||
}
|
||||
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.id = ckeditorThemeStyleId;
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
return styleElement;
|
||||
}
|
||||
|
||||
function syncCkeditorTheme(currentTheme) {
|
||||
var styleElement = getOrCreateCkeditorThemeStyleElement();
|
||||
|
||||
if (currentTheme !== 'dark') {
|
||||
styleElement.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
styleElement.textContent = [
|
||||
'.ck.ck-dropdown__panel,',
|
||||
'.ck.ck-list__panel,',
|
||||
'.ck.ck-list,',
|
||||
'.ck.ck-balloon-panel {',
|
||||
' background: var(--bs-body-bg) !important;',
|
||||
' background-color: var(--bs-body-bg) !important;',
|
||||
' border-color: var(--bs-border-color) !important;',
|
||||
' color: var(--bs-body-color) !important;',
|
||||
'}',
|
||||
'.ck.ck-list .ck-list-item-button {',
|
||||
' background: transparent !important;',
|
||||
' background-color: transparent !important;',
|
||||
' color: var(--bs-body-color) !important;',
|
||||
'}',
|
||||
'.ck.ck-list .ck-list-item-button:hover {',
|
||||
' background: var(--bs-secondary-bg) !important;',
|
||||
' background-color: var(--bs-secondary-bg) !important;',
|
||||
'}',
|
||||
'.ck.ck-color-grid,',
|
||||
'.ck.ck-color-grid__tile {',
|
||||
' color: var(--bs-body-color) !important;',
|
||||
'}',
|
||||
'.ck.ck-color-grid__tile {',
|
||||
' border-color: var(--bs-border-color) !important;',
|
||||
'}'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
try {
|
||||
var storedTheme = window.localStorage.getItem(storageKey);
|
||||
if (storedTheme === 'dark' || storedTheme === 'light' || storedTheme === 'auto') {
|
||||
@@ -73,5 +23,4 @@
|
||||
|
||||
document.documentElement.dataset.bsTheme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
syncCkeditorTheme(theme === 'auto' ? getPreferredTheme() : theme);
|
||||
}());
|
||||
+25
-11
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
(function (root) {
|
||||
function getToastTitle(variant) {
|
||||
var textVariant = String(variant || '').trim().toLowerCase();
|
||||
if (textVariant === 'danger') {
|
||||
@@ -141,12 +141,17 @@
|
||||
|
||||
function getMessageVariant(message, fallbackVariant) {
|
||||
var text = String(message || '').trim();
|
||||
if (/\b(?:unable to|cannot|can't|could not|failed to)\s+delete\b/i.test(text) || /\bdelete\b.*\b(?:before|first)\b/i.test(text) || /\bstill (?:in use|linked|assigned|used)\b/i.test(text)) {
|
||||
return 'danger';
|
||||
}
|
||||
if (/\b(?:already exists|already exist|already taken|duplicate|must be unique|name already exists)\b/i.test(text)) {
|
||||
return 'warning';
|
||||
var rules = [
|
||||
{ variant: 'danger', patterns: [/\b(?:unable to|cannot|can't|could not|failed to)\s+delete\b/i, /\bdelete\b.*\b(?:before|first)\b/i, /\bstill (?:in use|linked|assigned|used)\b/i] },
|
||||
{ variant: 'warning', patterns: [/\b(?:already exists|already exist|already taken|duplicate|must be unique|name already exists)\b/i] }
|
||||
];
|
||||
|
||||
for (var i = 0; i < rules.length; i += 1) {
|
||||
if (rules[i].patterns.some(function (pattern) { return pattern.test(text); })) {
|
||||
return rules[i].variant;
|
||||
}
|
||||
}
|
||||
|
||||
return String(fallbackVariant || 'info').trim().toLowerCase() || 'info';
|
||||
}
|
||||
|
||||
@@ -230,9 +235,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
window.dismissToast = dismissToast;
|
||||
window.showToast = showToast;
|
||||
window.initToast = initToast;
|
||||
root.dismissToast = dismissToast;
|
||||
root.showToast = showToast;
|
||||
root.initToast = initToast;
|
||||
root.getMessageVariant = getMessageVariant;
|
||||
|
||||
initToast();
|
||||
}());
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
getMessageVariant: getMessageVariant
|
||||
};
|
||||
}
|
||||
|
||||
if (root.document) {
|
||||
initToast();
|
||||
}
|
||||
}(typeof window !== 'undefined' ? window : globalThis));
|
||||
@@ -1,55 +0,0 @@
|
||||
Software License Agreement
|
||||
==========================
|
||||
|
||||
**CKEditor 5** (https://github.com/ckeditor/ckeditor5)<br>
|
||||
Copyright (c) 2003–2026, [CKSource Holding sp. z o.o.](https://cksource.com) All rights reserved.
|
||||
|
||||
Licensed under a dual-license model, this software is available under:
|
||||
|
||||
* the [GNU General Public License Version 2 or later](https://www.gnu.org/licenses/gpl.html) (see COPYING.GPL),
|
||||
* or commercial license terms from CKSource Holding sp. z o.o.
|
||||
|
||||
For more information, see: [https://ckeditor.com/legal/ckeditor-licensing-options](https://ckeditor.com/legal/ckeditor-licensing-options).
|
||||
|
||||
If you are using CKEditor under commercial terms, you are free to remove the COPYING.GPL file with the full copy of a GPL license.
|
||||
|
||||
Sources of Intellectual Property Included in CKEditor 5
|
||||
------------------------------------------------------------
|
||||
|
||||
Where not otherwise indicated, all CKEditor 5 content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor 5 will incorporate work done by developers outside of CKSource with their express permission.
|
||||
|
||||
The following libraries are included in CKEditor 5 under the [ISC license](https://opensource.org/licenses/ISC):
|
||||
|
||||
* hast-util-from-dom - Copyright (c) Keith McKnight <keith@mcknig.ht>.
|
||||
* rehype-dom-parse - Copyright (c) 2018 Keith McKnight <keith@mcknig.ht>.
|
||||
* rehype-dom-stringify - Copyright (c) 2018 Keith McKnight <keith@mcknig.ht>.
|
||||
|
||||
The following libraries are included in CKEditor 5 under the [MIT license](https://opensource.org/licenses/MIT):
|
||||
|
||||
* @types/color-convert - Copyright (c) Microsoft Corporation.
|
||||
* @types/hast - Copyright (c) Microsoft Corporation.
|
||||
* blurhash - Copyright (c) 2018 Wolt Enterprises.
|
||||
* color-convert - Copyright (c) 2011-2016 Heather Arthur <fayearthur@gmail.com> and Copyright (c) 2016-2021 Josh Junon <josh@junon.me>.
|
||||
* color-parse - Copyright (c) 2015 Dmitry Ivanov.
|
||||
* emojibase-data - Copyright (c) 2017-2019 Miles Johnson.
|
||||
* es-toolkit - Copyright (c) 2024 Viva Republica, Inc and Copyright OpenJS Foundation and other contributors.
|
||||
* fuzzysort - Copyright (c) 2018 Stephen Kamenar.
|
||||
* hast-util-to-html - Copyright (c) Titus Wormer <tituswormer@gmail.com>.
|
||||
* hast-util-to-mdast - Copyright (c) Titus Wormer <tituswormer@gmail.com> and Copyright (c) Seth Vincent <sethvincent@gmail.com>.
|
||||
* hastscript - Copyright (c) Titus Wormer <tituswormer@gmail.com>.
|
||||
* is-emoji-supported - Copyright (c) 2016-2020 Koala Interactive, Inc.
|
||||
* Regular expression for URL validation - Copyright (c) 2010-2018 Diego Perini.
|
||||
* rehype-remark - Copyright (c) Titus Wormer <tituswormer@gmail.com>.
|
||||
* remark-breaks - Copyright (c) 2017 Titus Wormer <tituswormer@gmail.com>.
|
||||
* remark-gfm - Copyright (c) Titus Wormer <tituswormer@gmail.com>.
|
||||
* remark-parse - Copyright (c) 2014 Titus Wormer <tituswormer@gmail.com>.
|
||||
* remark-rehype - Copyright (c) Titus Wormer <tituswormer@gmail.com>.
|
||||
* remark-stringify - Copyright (c) 2014 Titus Wormer <tituswormer@gmail.com>.
|
||||
* unified - Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>.
|
||||
* unist-util-visit - Copyright (c) 2015 Titus Wormer <tituswormer@gmail.com>.
|
||||
* vanilla-colorful - Copyright (c) 2020 Serhii Kulykov <iamkulykov@gmail.com>.
|
||||
|
||||
Trademarks
|
||||
----------
|
||||
|
||||
**CKEditor** is a trademark of [CKSource Holding sp. z o.o.](https://cksource.com) All other brand and product names are trademarks, registered trademarks or service marks of their respective holders.
|
||||
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
File diff suppressed because one or more lines are too long
-188
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-184
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
||||
*/
|
||||
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils';
|
||||
declare const translations: Translations;
|
||||
export default translations;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user