From cde9938845e55feaddf745788a00b1e5993878f3 Mon Sep 17 00:00:00 2001 From: OlgaLarina Date: Tue, 26 Sep 2023 14:38:57 +0300 Subject: [PATCH] title cover (#6976) * work for "Title cover" : step 1 * work for "Title cover" : step 2 add vertical aligment & * work for "Title cover" : step 3 add title & description components * work for "Title cover" : step 4 add background image * work for "Title cover" : step 5 save cover into theme * work for "Title cover" : step 6 change layouting * Work for #248 - Cover - implemented cover elements positioning * Fixed titleView default to pass tests - https://github.com/surveyjs/private-tasks/issues/248 * work for "Title cover" : small fixes * Work for https://github.com/surveyjs/private-tasks/issues/248 - implemented cover for react and vue2 * Fixed lint * Work for https://github.com/surveyjs/private-tasks/issues/248 - implemented cover for vue3 * Work for https://github.com/surveyjs/private-tasks/issues/248 - implemented cover for angular * Work for https://github.com/surveyjs/private-tasks/issues/248 - fixed text width (allow be greater than cell width) * Work for https://github.com/surveyjs/private-tasks/issues/248 - added cover in titleView setter + generate default cover * Work for https://github.com/surveyjs/private-tasks/issues/248 - added cover in titleView setter + generate default cover - added vr-test * Removed test.only * Work for https://github.com/surveyjs/private-tasks/issues/248 - try to fix PR * work for "Title cover" : update etalon & try fix vue3 * work for "Title cover" : fix vue3 * work for "Title cover" : exclude jquery for knockout tests * work for "Title cover" : exclude jquery for knockout tests * work for "Title cover" : fix angular layout --------- Co-authored-by: OlgaLarina Co-authored-by: tsv2013 Co-authored-by: tsv2013 --- examples_test/defaultV2/knockout.html | 2 +- .../src/angular-ui.module.ts | 9 +- packages/survey-angular-ui/src/angular-ui.ts | 2 + .../cover/cover-cell.component.html | 18 ++ .../cover/cover-cell.component.scss | 0 .../components/cover/cover-cell.component.ts | 17 ++ .../src/components/cover/cover.component.html | 10 + .../src/components/cover/cover.component.scss | 0 .../src/components/cover/cover.component.ts | 21 ++ .../src/survey-content.component.html | 2 +- packages/survey-vue3-ui/src/Survey.vue | 2 +- .../src/components/cover/Cover.vue | 27 +++ .../src/components/cover/CoverCell.vue | 35 +++ packages/survey-vue3-ui/src/index.ts | 6 + src/base-interfaces.ts | 2 + src/cover.ts | 176 +++++++++++++++ src/defaultV2-theme/blocks/cover.scss | 96 ++++++++ src/defaultV2-theme/blocks/sd-title.scss | 1 + src/defaultV2-theme/defaultV2.fontless.scss | 1 + src/entries/chunks/model.ts | 23 +- src/entries/knockout-ui-model.ts | 2 + src/entries/react-ui-model.ts | 1 + src/entries/vue-ui-model.ts | 2 + src/knockout/components/cover/cover-cell.html | 24 ++ src/knockout/components/cover/cover-cell.ts | 14 ++ src/knockout/components/cover/cover.html | 13 ++ src/knockout/components/cover/cover.ts | 15 ++ src/knockout/templates/index.html | 2 + src/react/components/cover.tsx | 78 +++++++ src/react/reactSurvey.tsx | 2 +- src/survey.ts | 43 +++- src/themes.ts | 1 + src/utils/utils.ts | 5 + src/vue/components/cover/cover-cell.vue | 44 ++++ src/vue/components/cover/cover.vue | 36 +++ src/vue/survey.vue | 2 +- tests/coverTests.ts | 207 ++++++++++++++++++ tests/entries/test.ts | 1 + tests/surveytests.ts | 4 +- .../etalons/survey-cover-default.png | Bin 0 -> 127756 bytes .../tests/defaultV2/survey.ts | 22 ++ 41 files changed, 945 insertions(+), 23 deletions(-) create mode 100644 packages/survey-angular-ui/src/components/cover/cover-cell.component.html create mode 100644 packages/survey-angular-ui/src/components/cover/cover-cell.component.scss create mode 100644 packages/survey-angular-ui/src/components/cover/cover-cell.component.ts create mode 100644 packages/survey-angular-ui/src/components/cover/cover.component.html create mode 100644 packages/survey-angular-ui/src/components/cover/cover.component.scss create mode 100644 packages/survey-angular-ui/src/components/cover/cover.component.ts create mode 100644 packages/survey-vue3-ui/src/components/cover/Cover.vue create mode 100644 packages/survey-vue3-ui/src/components/cover/CoverCell.vue create mode 100644 src/cover.ts create mode 100644 src/defaultV2-theme/blocks/cover.scss create mode 100644 src/knockout/components/cover/cover-cell.html create mode 100644 src/knockout/components/cover/cover-cell.ts create mode 100644 src/knockout/components/cover/cover.html create mode 100644 src/knockout/components/cover/cover.ts create mode 100644 src/react/components/cover.tsx create mode 100644 src/vue/components/cover/cover-cell.vue create mode 100644 src/vue/components/cover/cover.vue create mode 100644 tests/coverTests.ts create mode 100644 visualRegressionTests/tests/defaultV2/etalons/survey-cover-default.png diff --git a/examples_test/defaultV2/knockout.html b/examples_test/defaultV2/knockout.html index a71e3cc08c..b3403494f8 100644 --- a/examples_test/defaultV2/knockout.html +++ b/examples_test/defaultV2/knockout.html @@ -2,7 +2,7 @@ - + diff --git a/packages/survey-angular-ui/src/angular-ui.module.ts b/packages/survey-angular-ui/src/angular-ui.module.ts index 61e318d563..cb060f1459 100644 --- a/packages/survey-angular-ui/src/angular-ui.module.ts +++ b/packages/survey-angular-ui/src/angular-ui.module.ts @@ -78,7 +78,7 @@ import { RankingItemComponent } from "./questions/ranking-item.component"; import { SurveyStringComponent } from "./survey-string.component"; import { StringEditorComponent } from "./string-editor.component"; import { PanelDynamicAddBtn } from "./components/paneldynamic-actions/paneldynamic-add-btn.component"; -import { PanelDynamicNextBtn }from "./components/paneldynamic-actions/paneldynamic-next-btn.component"; +import { PanelDynamicNextBtn } from "./components/paneldynamic-actions/paneldynamic-next-btn.component"; import { PanelDynamicPrevBtn } from "./components/paneldynamic-actions/paneldynamic-prev-btn.component"; import { PanelDynamicProgressText } from "./components/paneldynamic-actions/paneldynamic-progress-text.component"; import { PanelDynamicQuestionComponent } from "./questions/paneldynamic.component"; @@ -112,7 +112,10 @@ import { NotifierComponent } from "./components/notifier/notifier.component"; import { ComponentsContainerComponent } from "./components-container.component"; import { MultipleTextRowComponent } from "./questions/multipletextrow.component"; import { LoadingIndicatorComponent } from "./angular-ui"; +import { CoverComponent } from "./components/cover/cover.component"; +import { CoverCellComponent } from "./components/cover/cover-cell.component"; import { ChooseFileBtn } from "./components/file-actions/choose-file.component"; + @NgModule({ declarations: [ VisibleDirective, Key2ClickDirective, PanelDynamicAddBtn, PanelDynamicNextBtn, PanelDynamicPrevBtn, PanelDynamicProgressText, ElementComponent, TemplateRendererComponent, @@ -129,7 +132,7 @@ import { ChooseFileBtn } from "./components/file-actions/choose-file.component"; MultipleTextComponent, MultipleTextItemComponent, DynamicComponentDirective, RankingQuestionComponent, RankingItemComponent, PanelDynamicQuestionComponent, EmbeddedViewContentComponent, CustomWidgetComponent, MatrixCellComponent, MatrixTableComponent, MatrixDropdownComponent, MatrixDynamicComponent, MatrixDetailButtonComponent, MatrixDynamicRemoveButtonComponent, MatrixDynamicDragDropIconComponent, MatrixRequiredHeader, ExpressionComponent, SafeResourceUrlPipe, BrandInfoComponent, CustomQuestionComponent, CompositeQuestionComponent, ButtonGroupItemComponent, ButtonGroupQuestionComponent, MatrixRowComponent, ModalComponent, LogoImageComponent, SkeletonComponent, TimerPanelComponent, PaneldynamicRemoveButtonComponent, - NotifierComponent, ComponentsContainerComponent, MultipleTextRowComponent, LoadingIndicatorComponent, ChooseFileBtn + NotifierComponent, ComponentsContainerComponent, MultipleTextRowComponent, LoadingIndicatorComponent, CoverComponent, CoverCellComponent, ChooseFileBtn ], imports: [ CommonModule, FormsModule @@ -150,7 +153,7 @@ import { ChooseFileBtn } from "./components/file-actions/choose-file.component"; MultipleTextComponent, MultipleTextItemComponent, DynamicComponentDirective, RankingQuestionComponent, RankingItemComponent, PanelDynamicQuestionComponent, EmbeddedViewContentComponent, CustomWidgetComponent, MatrixCellComponent, MatrixTableComponent, MatrixDropdownComponent, MatrixDynamicComponent, MatrixDetailButtonComponent, MatrixDynamicRemoveButtonComponent, MatrixDynamicDragDropIconComponent, MatrixRequiredHeader, ExpressionComponent, SafeResourceUrlPipe, CustomQuestionComponent, CompositeQuestionComponent, ButtonGroupQuestionComponent, ModalComponent, LogoImageComponent, SkeletonComponent, TimerPanelComponent, PaneldynamicRemoveButtonComponent, - NotifierComponent, ComponentsContainerComponent, MultipleTextRowComponent, LoadingIndicatorComponent + NotifierComponent, ComponentsContainerComponent, MultipleTextRowComponent, LoadingIndicatorComponent, CoverComponent, CoverCellComponent ], providers: [PopupService], }) diff --git a/packages/survey-angular-ui/src/angular-ui.ts b/packages/survey-angular-ui/src/angular-ui.ts index 3b66bdca1c..ab1b024fd4 100644 --- a/packages/survey-angular-ui/src/angular-ui.ts +++ b/packages/survey-angular-ui/src/angular-ui.ts @@ -108,6 +108,8 @@ export * from "./questions/custom.component"; export * from "./questions/composite.component"; export * from "./base-angular"; export * from "./components/loading-indicator/loading-indicator.component"; +export * from "./components/cover/cover.component"; +export * from "./components/cover/cover-cell.component"; export * from "./component-factory"; export * from "./angular-ui.module"; \ No newline at end of file diff --git a/packages/survey-angular-ui/src/components/cover/cover-cell.component.html b/packages/survey-angular-ui/src/components/cover/cover-cell.component.html new file mode 100644 index 0000000000..d827f6e4d0 --- /dev/null +++ b/packages/survey-angular-ui/src/components/cover/cover-cell.component.html @@ -0,0 +1,18 @@ + + +
+
+ +
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/packages/survey-angular-ui/src/components/cover/cover-cell.component.scss b/packages/survey-angular-ui/src/components/cover/cover-cell.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/survey-angular-ui/src/components/cover/cover-cell.component.ts b/packages/survey-angular-ui/src/components/cover/cover-cell.component.ts new file mode 100644 index 0000000000..b609f7c062 --- /dev/null +++ b/packages/survey-angular-ui/src/components/cover/cover-cell.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, ViewChild, ViewContainerRef } from "@angular/core"; +import { BaseAngular } from "../../base-angular"; +import { EmbeddedViewContentComponent } from "../../embedded-view-content.component"; +import { AngularComponentFactory } from "../../component-factory"; +import { Cover, CoverCell, SurveyModel } from "survey-core"; + +@Component({ + selector: "sv-ng-cover-cell", + templateUrl: "./cover-cell.component.html", + styles: [":host { display: none; }"], +}) +export class CoverCellComponent extends EmbeddedViewContentComponent { + @Input() model!: CoverCell; + @ViewChild("actionContent", { read: ViewContainerRef, static: true }) actionContent!: ViewContainerRef; +} + +AngularComponentFactory.Instance.registerComponent("sv-cover-cell", CoverCellComponent); \ No newline at end of file diff --git a/packages/survey-angular-ui/src/components/cover/cover.component.html b/packages/survey-angular-ui/src/components/cover/cover.component.html new file mode 100644 index 0000000000..1f688c24b8 --- /dev/null +++ b/packages/survey-angular-ui/src/components/cover/cover.component.html @@ -0,0 +1,10 @@ + +
+
+
+ + + +
+
+
\ No newline at end of file diff --git a/packages/survey-angular-ui/src/components/cover/cover.component.scss b/packages/survey-angular-ui/src/components/cover/cover.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/survey-angular-ui/src/components/cover/cover.component.ts b/packages/survey-angular-ui/src/components/cover/cover.component.ts new file mode 100644 index 0000000000..6a0994fa38 --- /dev/null +++ b/packages/survey-angular-ui/src/components/cover/cover.component.ts @@ -0,0 +1,21 @@ +import { Component, ElementRef, Input, ViewChild } from "@angular/core"; +import { AngularComponentFactory } from "../../component-factory"; +import { BaseAngular } from "../../base-angular"; +import { Cover, CoverCell, SurveyModel } from "survey-core"; + +@Component({ + selector: "sv-cover, sv-ng-cover", + templateUrl: "./cover.component.html", + styles: [":host { display: none }"] +}) +export class CoverComponent extends BaseAngular { + @Input() model!: Cover; + @Input() survey!: SurveyModel; + @ViewChild("container") container!: ElementRef; + getModel(): Cover { + this.model.survey = this.survey; + return this.model; + } +} + +AngularComponentFactory.Instance.registerComponent("sv-cover", CoverComponent); \ No newline at end of file diff --git a/packages/survey-angular-ui/src/survey-content.component.html b/packages/survey-angular-ui/src/survey-content.component.html index 0b6dfd81db..31326f9ba9 100644 --- a/packages/survey-angular-ui/src/survey-content.component.html +++ b/packages/survey-angular-ui/src/survey-content.component.html @@ -4,7 +4,7 @@
-
+
diff --git a/packages/survey-vue3-ui/src/Survey.vue b/packages/survey-vue3-ui/src/Survey.vue index 400622df58..189bc17fd0 100644 --- a/packages/survey-vue3-ui/src/Survey.vue +++ b/packages/survey-vue3-ui/src/Survey.vue @@ -13,7 +13,7 @@
- + +
+
+
+ +
+
+ + + diff --git a/packages/survey-vue3-ui/src/components/cover/CoverCell.vue b/packages/survey-vue3-ui/src/components/cover/CoverCell.vue new file mode 100644 index 0000000000..0d383843fc --- /dev/null +++ b/packages/survey-vue3-ui/src/components/cover/CoverCell.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/survey-vue3-ui/src/index.ts b/packages/survey-vue3-ui/src/index.ts index 8d214f74d1..52d16600fc 100644 --- a/packages/survey-vue3-ui/src/index.ts +++ b/packages/survey-vue3-ui/src/index.ts @@ -102,6 +102,9 @@ import Custom from "./Custom.vue"; import TimerPanel from "./TimerPanel.vue"; import LoadingIndicator from "./components/LoadingIndicator.vue"; +import Cover from "./components/cover/Cover.vue"; +import CoverCell from "./components/cover/CoverCell.vue"; + import { SurveyModel, doKey2ClickBlur, @@ -236,6 +239,9 @@ function registerComponents(app: App) { app.component("sv-timerpanel", TimerPanel); app.component("sv-loading-indicator", LoadingIndicator); + app.component("sv-cover", Cover); + app.component("sv-cover-cell", CoverCell); + app.directive("key2click", { // When the bound element is inserted into the DOM... mounted: function (el: HTMLElement, binding: any) { diff --git a/src/base-interfaces.ts b/src/base-interfaces.ts index 6e92c14f03..428e5c8a04 100644 --- a/src/base-interfaces.ts +++ b/src/base-interfaces.ts @@ -366,6 +366,8 @@ export type ISurveyEnvironment = { } export type LayoutElementContainer = "header" | "footer" | "left" | "right" | "contentTop" | "contentBottom"; +export type HorizontalAlignment = "left" | "center" | "right"; +export type VerticalAlignment = "top" | "middle" | "bottom"; export interface ISurveyLayoutElement { id: string; diff --git a/src/cover.ts b/src/cover.ts new file mode 100644 index 0000000000..837d1dd464 --- /dev/null +++ b/src/cover.ts @@ -0,0 +1,176 @@ +import { Base } from "./base"; +import { HorizontalAlignment, VerticalAlignment } from "./base-interfaces"; +import { Serializer, property } from "./jsonobject"; +import { SurveyModel } from "./survey"; +import { CssClassBuilder } from "./utils/cssClassBuilder"; +import { wrapUrlForBackgroundImage } from "./utils/utils"; + +export class CoverCell { + static CLASSNAME = "sv-cover__cell"; + private calcRow(positionY: VerticalAlignment): any { + return positionY === "top" ? 1 : (positionY === "middle" ? 2 : 3); + } + private calcColumn(positionX: HorizontalAlignment): any { + return positionX === "left" ? 1 : (positionX === "center" ? 2 : 3); + } + private calcAlignItems(positionX: HorizontalAlignment) { + return positionX === "left" ? "flex-start" : (positionX === "center" ? "center" : "flex-end"); + } + private calcAlignText(positionX: HorizontalAlignment) { + return positionX === "left" ? "start" : (positionX === "center" ? "center" : "end"); + } + private calcJustifyContent(positionY: VerticalAlignment) { + return positionY === "top" ? "flex-start" : (positionY === "middle" ? "center" : "flex-end"); + } + + constructor(private cover: Cover, private positionX: HorizontalAlignment, private positionY: VerticalAlignment) { + } + get survey(): SurveyModel { + return this.cover.survey; + } + get css(): string { + const result = `${CoverCell.CLASSNAME} ${CoverCell.CLASSNAME}--${this.positionX} ${CoverCell.CLASSNAME}--${this.positionY}`; + return result; + } + get style(): any { + const result: any = {}; + result["gridColumn"] = this.calcColumn(this.positionX); + result["gridRow"] = this.calcRow(this.positionY); + return result; + } + get contentStyle(): any { + const result: any = {}; + result["textAlign"] = this.calcAlignText(this.positionX); + result["alignItems"] = this.calcAlignItems(this.positionX); + result["justifyContent"] = this.calcJustifyContent(this.positionY); + return result; + } + get showLogo(): boolean { + return this.survey.hasLogo && this.positionX === this.cover.logoPositionX && this.positionY === this.cover.logoPositionY; + } + get showTitle(): boolean { + return this.survey.hasTitle && this.positionX === this.cover.titlePositionX && this.positionY === this.cover.titlePositionY; + } + get showDescription(): boolean { + return this.survey.renderedHasDescription && this.positionX === this.cover.descriptionPositionX && this.positionY === this.cover.descriptionPositionY; + } + get textWidth(): string { + if (!this.cover.textWidth) { + return ""; + } + return "" + this.cover.textWidth + "px"; + } +} + +export class Cover extends Base { + private calcBackgroundSize(backgroundImageFit: "cover" | "fill" | "contain" | "tile"): string { + if (backgroundImageFit === "fill") { + return "100% 100%"; + } + if (backgroundImageFit === "tile") { + return "contain"; + } + return backgroundImageFit; + } + + constructor() { + super(); + this.renderBackgroundImage = wrapUrlForBackgroundImage(this.backgroundImage); + ["top", "middle", "bottom"].forEach((positionY: VerticalAlignment) => + ["left", "center", "right"].forEach((positionX: HorizontalAlignment) => this.cells.push(new CoverCell(this, positionX, positionY))) + ); + } + + public getType(): string { + return "cover"; + } + public survey: SurveyModel; + public cells: CoverCell[] = []; + @property() public height: number; + @property() public areaWidth: "survey" | "container"; + @property() public textWidth: number; + @property() public invertText: boolean; + @property() public glowText: boolean; + @property() public overlap: boolean; + @property() public backgroundColor: string; + @property({ + onSet: (newVal: string, target: Cover) => { + target.renderBackgroundImage = wrapUrlForBackgroundImage(newVal); + } + }) public backgroundImage: string; + @property() public renderBackgroundImage: string; + @property() public backgroundImageFit: "cover" | "fill" | "contain" | "tile"; + @property() public backgroundImageOpacity: number; + @property() public logoPositionX: HorizontalAlignment; + @property() public logoPositionY: VerticalAlignment; + @property() public titlePositionX: HorizontalAlignment; + @property() public titlePositionY: VerticalAlignment; + @property() public descriptionPositionX: HorizontalAlignment; + @property() public descriptionPositionY: VerticalAlignment; + @property() logoStyle: { gridColumn: number, gridRow: number }; + @property() titleStyle: { gridColumn: number, gridRow: number }; + @property() descriptionStyle: { gridColumn: number, gridRow: number }; + + public get renderedHeight(): string { + return this.height ? this.height + "px" : undefined; + } + public get renderedTextWidth(): string { + return this.textWidth ? this.textWidth + "px" : undefined; + } + + public get coverClasses(): string { + return new CssClassBuilder() + .append("sv-cover") + .append("sv-conver__without-background", !this.backgroundColor && !this.backgroundImage) + .toString(); + } + public get contentClasses(): string { + return new CssClassBuilder() + .append("sv-conver__content") + .append("sv-conver__content--static", this.areaWidth === "survey" && this.survey.calculateWidthMode() === "static") + .append("sv-conver__content--responsive", this.areaWidth === "container" || this.survey.calculateWidthMode() === "responsive") + .toString(); + } + + public get backgroundImageClasses(): string { + return new CssClassBuilder() + .append("sv-cover__background-image") + .append("sv-cover__background-image--contain", this.backgroundImageFit === "contain") + .append("sv-cover__background-image--tile", this.backgroundImageFit === "tile") + .toString(); + } + public get backgroundImageStyle() { + if (!this.backgroundImage) return null; + return { + opacity: this.backgroundImageOpacity, + backgroundImage: this.renderBackgroundImage, + backgroundSize: this.calcBackgroundSize(this.backgroundImageFit), + }; + } +} + +Serializer.addClass( + "cover", + [ + { name: "height:number", minValue: 0, default: 256 }, + { name: "areaWidth", default: "survey" }, + { name: "textWidth:number", minValue: 0, default: 512 }, + { name: "invertText:boolean" }, + { name: "glowText:boolean" }, + { name: "overlap:boolean" }, + { name: "backgroundColor" }, + { name: "backgroundImage" }, + { name: "backgroundImageOpacity:number", minValue: 0, maxValue: 1, default: 1 }, + { name: "backgroundImageFit", default: "cover", choices: ["cover", "fill", "contain"] }, + { name: "logoPositionX", default: "right" }, + { name: "logoPositionY", default: "top" }, + { name: "titlePositionX", default: "left" }, + { name: "titlePositionY", default: "bottom" }, + { name: "descriptionPositionX", default: "left" }, + { name: "descriptionPositionY", default: "bottom" } + + ], + function () { + return new Cover(); + }, +); \ No newline at end of file diff --git a/src/defaultV2-theme/blocks/cover.scss b/src/defaultV2-theme/blocks/cover.scss new file mode 100644 index 0000000000..6614b661d8 --- /dev/null +++ b/src/defaultV2-theme/blocks/cover.scss @@ -0,0 +1,96 @@ +.sv-cover { + padding: calcSize(5); + box-sizing: border-box; + position: relative; +} + +.sv-conver__without-background { + padding-bottom: 0; +} + +.sv-conver__content { + height: 100%; + position: relative; + display: grid; + gap: 0; + // grid-auto-rows: min-content; + grid-auto-columns: 1fr 1fr 1fr; + grid-auto-rows: 1fr 1fr 1fr; +} + +.sv-conver__content--static { + max-width: calcSize(80); + margin-left: auto; + margin-right: auto; +} + +.sv-cover__background-image { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + right: 0; + border: 0; + background-position-x: center; +} + +.sv-cover__background-image--contain { + background-repeat: no-repeat; +} + +.sv-cover__cell { + position: relative; +} + +.sv-cover__cell-content { + display: flex; + flex-direction: column; + position: absolute; + width: max-content; + top: 0; + bottom: 0; +} + +.sv-cover__cell--left { + .sv-cover__cell-content { + left: 0; + } +} + +// .sv-cover__cell--center { +// .sv-cover__cell-content { +// left: 0; +// right: 0; +// } +// } + +.sv-cover__cell--right { + .sv-cover__cell-content { + right: 0; + } +} + +.sv-cover__logo { + display: flex; +} + +.sv-cover__title { + display: flex; +} + +.sv-cover__title .sd-title { + color: $foreground-dim; + font-family: $font-surveytitle-family; + font-size: $font-surveytitle-size; + font-weight: $font-surveytitle-weight; + line-height: multiply(1.25, $font-surveytitle-size); +} + +.sv-cover__description { + display: flex; +} + +.sv-cover__description .sd-description { + color: $foreground-dim-light; +} \ No newline at end of file diff --git a/src/defaultV2-theme/blocks/sd-title.scss b/src/defaultV2-theme/blocks/sd-title.scss index d84d8ade4f..a81134c75b 100644 --- a/src/defaultV2-theme/blocks/sd-title.scss +++ b/src/defaultV2-theme/blocks/sd-title.scss @@ -62,6 +62,7 @@ } .sd-root-modern { + .sv-conver__content, .sd-container-modern__title { .sd-header__text h3 { margin: 0; diff --git a/src/defaultV2-theme/defaultV2.fontless.scss b/src/defaultV2-theme/defaultV2.fontless.scss index 859b8236e8..de56e07999 100644 --- a/src/defaultV2-theme/defaultV2.fontless.scss +++ b/src/defaultV2-theme/defaultV2.fontless.scss @@ -45,6 +45,7 @@ @import "blocks/sd-progress-toc.scss"; @import "blocks/sd-list.scss"; @import "blocks/sd-timer.scss"; +@import "blocks/cover.scss"; @import "blocks/sd-loading-indicator.scss"; @import "../components-container.scss"; @import "../signaturepad.scss"; diff --git a/src/entries/chunks/model.ts b/src/entries/chunks/model.ts index 7b0ef1a8e2..e883166fbf 100644 --- a/src/entries/chunks/model.ts +++ b/src/entries/chunks/model.ts @@ -8,7 +8,7 @@ Version = `${process.env.VERSION}`; ReleaseDate = `${process.env.RELEASE_DATE}`; export function checkLibraryVersion(ver: string, libraryName: string): void { - if(Version != ver) { + if (Version != ver) { const str = "survey-core has version '" + Version + "' and " + libraryName + " has version '" + ver + "'. SurveyJS libraries should have the same versions to work correctly."; /* eslint no-console: ["error", { allow: ["error"] }] */ @@ -23,25 +23,25 @@ export function hasLicense(index: number): boolean { } const lic: any = {}; function slk(k: any, lh: any, rd: any) { - if(!k) return; + if (!k) return; const en = (s: string) => { - var e: any={}, i, b=0, c, x, l=0, a, r="", w=String.fromCharCode, L=s.length; - var A="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for(i=0; i<64; i++) { e[A.charAt(i)]=i; } - for(x=0; x=8) { ((a=(b>>>(l-=8))&0xff)||(x<(L-2)))&&(r+=w(a)); } + var e: any = {}, i, b = 0, c, x, l = 0, a, r = "", w = String.fromCharCode, L = s.length; + var A = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (i = 0; i < 64; i++) { e[A.charAt(i)] = i; } + for (x = 0; x < L; x++) { + let c = e[s.charAt(x)]; b = (b << 6) + c; l += 6; + while (l >= 8) { ((a = (b >>> (l -= 8)) & 0xff) || (x < (L - 2))) && (r += w(a)); } } return r; }; let v = en(k); - if(!v) return; + if (!v) return; let index = v.indexOf(";"); - if(index < 0) return; + if (index < 0) return; v = v.substring(index + 1); v.split(",").forEach(s => { let i = s.indexOf("="); - if(i > 0) { + if (i > 0) { lh[s.substring(0, i)] = new Date(rd) <= new Date(s.substring(i + 1)); } }); @@ -210,6 +210,7 @@ export { export { PopupSurveyModel, SurveyWindowModel } from "../../popup-survey"; export { TextPreProcessor } from "../../textPreProcessor"; export { Notifier } from "../../notifier"; +export { Cover, CoverCell } from "../../cover"; export { dxSurveyService } from "../../dxSurveyService"; export { englishStrings } from "../../localization/english"; diff --git a/src/entries/knockout-ui-model.ts b/src/entries/knockout-ui-model.ts index 091b1e4fb0..15b20fc19e 100644 --- a/src/entries/knockout-ui-model.ts +++ b/src/entries/knockout-ui-model.ts @@ -67,6 +67,8 @@ export * from "../knockout/components/dropdown/dropdown"; export * from "../knockout/components/dropdown-select/dropdown-select"; export * from "../knockout/components/tagbox/tagbox-item"; export * from "../knockout/components/tagbox/tagbox"; +export * from "../knockout/components/cover/cover"; +export * from "../knockout/components/cover/cover-cell"; export * from "../knockout/components/file-actions/choose-file"; export * from "../knockout/components/list/list"; diff --git a/src/entries/react-ui-model.ts b/src/entries/react-ui-model.ts index 9f4a01e26c..e9c73e2f1e 100644 --- a/src/entries/react-ui-model.ts +++ b/src/entries/react-ui-model.ts @@ -98,6 +98,7 @@ export { Skeleton } from "../react/components/skeleton"; export { NotifierComponent } from "../react/components/notifier"; export { ComponentsContainer } from "../react/components/components-container"; export { CharacterCounterComponent } from "../react/components/character-counter"; +export * from "../react/components/cover"; export { SurveyLocStringViewer } from "../react/string-viewer"; export { SurveyLocStringEditor } from "../react/string-editor"; diff --git a/src/entries/vue-ui-model.ts b/src/entries/vue-ui-model.ts index 88279ed50f..d98f62ff14 100644 --- a/src/entries/vue-ui-model.ts +++ b/src/entries/vue-ui-model.ts @@ -101,6 +101,8 @@ export { NotifierComponent } from "../vue/components/notifier.vue"; export { ComponentsContainer } from "../vue/components/container.vue"; export { CharacterCounterComponent } from "../vue/components/character-counter.vue"; export { LoadingIndicatorComponent } from "../vue/components/loading-indicator.vue"; +export { CoverCellViewModel } from "../vue/components/cover/cover-cell.vue"; +export { CoverViewModel } from "../vue/components/cover/cover.vue"; import { SurveyModel } from "survey-core"; diff --git a/src/knockout/components/cover/cover-cell.html b/src/knockout/components/cover/cover-cell.html new file mode 100644 index 0000000000..2cf621660a --- /dev/null +++ b/src/knockout/components/cover/cover-cell.html @@ -0,0 +1,24 @@ +
+
+ + + + +
+ + +
+ + +
+
+ + +
+
+ +
+
diff --git a/src/knockout/components/cover/cover-cell.ts b/src/knockout/components/cover/cover-cell.ts new file mode 100644 index 0000000000..1ec32695b6 --- /dev/null +++ b/src/knockout/components/cover/cover-cell.ts @@ -0,0 +1,14 @@ +import * as ko from "knockout"; +import { ImplementorBase } from "../../kobase"; + +const template = require("./cover-cell.html"); + +ko.components.register("sv-cover-cell", { + viewModel: { + createViewModel: (params: any, componentInfo: any) => { + // new ImplementorBase(params.model); + return params.model; + }, + }, + template: template, +}); diff --git a/src/knockout/components/cover/cover.html b/src/knockout/components/cover/cover.html new file mode 100644 index 0000000000..2886e89d0e --- /dev/null +++ b/src/knockout/components/cover/cover.html @@ -0,0 +1,13 @@ + +
+ +
+ +
+ + + + +
+
+ \ No newline at end of file diff --git a/src/knockout/components/cover/cover.ts b/src/knockout/components/cover/cover.ts new file mode 100644 index 0000000000..b41014c2f6 --- /dev/null +++ b/src/knockout/components/cover/cover.ts @@ -0,0 +1,15 @@ +import * as ko from "knockout"; +import { ImplementorBase } from "../../kobase"; + +const template = require("./cover.html"); + +ko.components.register("sv-cover", { + viewModel: { + createViewModel: (params: any, componentInfo: any) => { + params.model.survey = params.survey; + new ImplementorBase(params.model); + return params; + }, + }, + template: template, +}); diff --git a/src/knockout/templates/index.html b/src/knockout/templates/index.html index f44244e5b4..07602eb749 100644 --- a/src/knockout/templates/index.html +++ b/src/knockout/templates/index.html @@ -11,8 +11,10 @@
+ +
diff --git a/src/react/components/cover.tsx b/src/react/components/cover.tsx new file mode 100644 index 0000000000..d16b9bfd5e --- /dev/null +++ b/src/react/components/cover.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { Base, SurveyModel, Cover, CoverCell } from "survey-core"; +import { SurveyElementBase } from "../reactquestion_element"; +import { ReactElementFactory } from "../element-factory"; +import { TitleElement } from "./title/title-element"; + +export interface ILayoutElementProps { + survey: SurveyModel; + model: T; +} + +export class CoverCellComponent extends React.Component { + get model(): CoverCell { + return this.props.model; + } + private renderLogoImage(): JSX.Element | null { + const componentName: string = this.model.survey.getElementWrapperComponentName( + this.model.survey, + "logo-image" + ); + const componentData: any = this.model.survey.getElementWrapperComponentData( + this.model.survey, + "logo-image" + ); + return ReactElementFactory.Instance.createElement(componentName, { + data: componentData, + }); + } + + render(): JSX.Element | null { + return (
+
+ {this.model.showLogo ? (
+ {this.renderLogoImage()} +
) : null} + {this.model.showTitle ? (
+ {/* {ReactElementFactory.Instance.createElement("survey-element-title", { element: this.model.survey })} */} + +
) : null} + {this.model.showDescription ? (
+
+ {SurveyElementBase.renderLocString(this.model.survey.locDescription)} +
+
) : null} +
+
); + } +} + +export class CoverComponent extends SurveyElementBase, any> { + get model(): Cover { + return this.props.model; + } + protected getStateElement(): Base { + return this.model; + } + + renderElement(): JSX.Element | null { + this.model.survey = this.props.survey; + + if(!(this.props.survey.titleView === "cover" && this.props.survey.renderedHasHeader)) { + return null; + } + + return ( +
+ {this.model.backgroundImage ?
: null} +
+ {this.model.cells.map((cell, index) => )} +
+
+ ); + } +} + +ReactElementFactory.Instance.registerElement("sv-cover", (props) => { + return React.createElement(CoverComponent, props); +}); \ No newline at end of file diff --git a/src/react/reactSurvey.tsx b/src/react/reactSurvey.tsx index 3c18bb6373..7d13f20889 100644 --- a/src/react/reactSurvey.tsx +++ b/src/react/reactSurvey.tsx @@ -98,7 +98,7 @@ export class Survey extends SurveyElementBase renderResult = this.renderSurvey(); } const backgroundImage = !!this.survey.renderBackgroundImage ?
: null; - const header: JSX.Element = ; + const header: JSX.Element | null = this.survey.titleView === "title" ? : null; const onSubmit = function (event: React.FormEvent) { event.preventDefault(); }; diff --git a/src/survey.ts b/src/survey.ts index 6d1d747750..e503fc6c21 100644 --- a/src/survey.ts +++ b/src/survey.ts @@ -43,7 +43,7 @@ import { } from "./expressionItems"; import { ExpressionRunner, ConditionRunner } from "./conditions"; import { settings } from "./settings"; -import { isContainerVisible, isMobile, mergeValues, scrollElementByChildId, navigateToUrl, getRenderedStyleSize, getRenderedSize } from "./utils/utils"; +import { isContainerVisible, isMobile, mergeValues, scrollElementByChildId, navigateToUrl, getRenderedStyleSize, getRenderedSize, wrapUrlForBackgroundImage } from "./utils/utils"; import { SurveyError } from "./survey-error"; import { IAction, Action } from "./actions/action"; import { ActionContainer, defaultActionBarCss } from "./actions/container"; @@ -71,6 +71,7 @@ import { QuestionFileModel } from "./question_file"; import { QuestionMultipleTextModel } from "./question_multipletext"; import { ITheme, ImageFit, ImageAttachment } from "./themes"; import { PopupModel } from "./popup"; +import { Cover } from "./cover"; /** * The `SurveyModel` object contains properties and methods that allow you to control the survey and access its elements. @@ -1157,6 +1158,31 @@ export class SurveyModel extends SurveyElementCore @property() loadingBodyCss: string; @property() containerCss: string; @property({ onSet: (newValue, target: SurveyModel) => { target.updateCss(); } }) fitToContainer: boolean; + @property({ + onSet: (newValue, target: SurveyModel) => { + if (newValue === "cover") { + const layoutElement = target.layoutElements.filter(a => a.id === newValue)[0]; + if (!layoutElement) { + var cover = new Cover(); + cover.logoPositionX = target.logoPosition === "right" ? "right" : "left"; + cover.logoPositionY = "middle"; + cover.titlePositionX = target.logoPosition === "right" ? "left" : "right"; + cover.titlePositionY = "middle"; + cover.descriptionPositionX = target.logoPosition === "right" ? "left" : "right"; + cover.descriptionPositionY = "middle"; + cover.survey = target; + target.layoutElements.unshift({ + id: "cover", + container: "header", + component: "sv-cover", + data: cover + }); + } + } else { + target.removeLayoutElement("cover"); + } + } + }) titleView: "cover" | "title"; private getNavigationCss(main: string, btn: string) { return new CssClassBuilder().append(main) @@ -2068,7 +2094,7 @@ export class SurveyModel extends SurveyElementCore @property() renderBackgroundImage: string; private updateRenderBackgroundImage(): void { const path = this.backgroundImage; - this.renderBackgroundImage = !!path ? ["url(", path, ")"].join("") : ""; + this.renderBackgroundImage = wrapUrlForBackgroundImage(path); } @property() backgroundImageFit: ImageFit; @property() backgroundImageAttachment: ImageAttachment; @@ -7289,7 +7315,19 @@ export class SurveyModel extends SurveyElementCore public applyTheme(theme: ITheme): void { if (!theme) return; + Object.keys(theme).forEach((key: keyof ITheme) => { + if (key === "cover") { + this.removeLayoutElement("cover"); + const newCoverModel = new Cover(); + newCoverModel.fromJSON(theme[key]); + this.layoutElements.push({ + id: "cover", + container: "header", + component: "sv-cover", + data: newCoverModel + }); + } if (key === "isPanelless") { this.isCompact = theme[key]; } else { @@ -7578,6 +7616,7 @@ Serializer.addClass("survey", [ }, { name: "width", visibleIf: (obj: any) => { return obj.widthMode === "static"; } }, { name: "fitToContainer:boolean", default: false }, + { name: "titleView", default: "title", choices: ["title", "cover"], visible: false }, { name: "backgroundImage", visible: false }, { name: "backgroundImageFit", default: "cover", choices: ["auto", "contain", "cover"], visible: false }, { name: "backgroundImageAttachment", default: "scroll", choices: ["scroll", "fixed"], visible: false }, diff --git a/src/themes.ts b/src/themes.ts index d93614c586..5f302c14e0 100644 --- a/src/themes.ts +++ b/src/themes.ts @@ -9,5 +9,6 @@ export interface ITheme { backgroundImageFit?: ImageFit; backgroundImageAttachment?: ImageAttachment; backgroundOpacity?: number; + cover?: {[index: string]: any}; cssVariables?: { [index: string]: string }; } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 59a58f84a4..585b7c2c90 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -159,6 +159,10 @@ function navigateToUrl(url: string): void { window.location.href = url; } +function wrapUrlForBackgroundImage(url: string): string { + return !!url ? ["url(", url, ")"].join("") : ""; +} + function getIconNameFromProxy(iconName: string): string { if (!iconName) return iconName; var proxyName = (settings.customIcons)[iconName]; @@ -413,6 +417,7 @@ export { findScrollableParent, scrollElementByChildId, navigateToUrl, + wrapUrlForBackgroundImage, createSvg, getIconNameFromProxy, increaseHeightByContent, diff --git a/src/vue/components/cover/cover-cell.vue b/src/vue/components/cover/cover-cell.vue new file mode 100644 index 0000000000..93c02d3c81 --- /dev/null +++ b/src/vue/components/cover/cover-cell.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/vue/components/cover/cover.vue b/src/vue/components/cover/cover.vue new file mode 100644 index 0000000000..fc32e3d794 --- /dev/null +++ b/src/vue/components/cover/cover.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/vue/survey.vue b/src/vue/survey.vue index 98d70ba54a..170f4af601 100644 --- a/src/vue/survey.vue +++ b/src/vue/survey.vue @@ -6,7 +6,7 @@
- +