Skip to content

Commit

Permalink
Merge pull request #8268 from surveyjs/feature/ranking-animations
Browse files Browse the repository at this point in the history
  • Loading branch information
dk981234 committed May 18, 2024
2 parents 986f678 + 7add6a6 commit 2d9c7a8
Show file tree
Hide file tree
Showing 20 changed files with 561 additions and 289 deletions.
10 changes: 5 additions & 5 deletions packages/survey-angular-ui/src/questions/ranking.component.html
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<div *ngIf="!model.selectToRankEnabled" [class]="model.rootClass" #contentElement>
<ng-container *ngFor="let item of model.rankingChoices; index as index; trackBy: trackItemBy">
<ng-container *ngFor="let item of model.renderedRankingChoices; index as index; trackBy: trackItemBy">
<ng-template [component]="{ name: getItemValueComponentName(item), data: getItemValueComponentData(item, index) }"></ng-template>
</ng-container>
</div>

<div *ngIf="model.selectToRankEnabled" [class]="model.rootClass" #contentElement>
<div [class]='model.getContainerClasses("from")' data-ranking="from-container">
<ng-container *ngFor="let item of model.unRankingChoices; index as index; trackBy: trackItemBy">
<ng-container *ngFor="let item of model.renderedUnRankingChoices; index as index; trackBy: trackItemBy">
<ng-template [component]="{ name: getItemValueComponentName(item), data: getItemValueComponentData(item, index, true) }"></ng-template>
</ng-container>

<div *ngIf="model.unRankingChoices.length === 0" [class]="model.cssClasses.containerPlaceholder" [model]="$any(model).locSelectToRankEmptyRankedAreaText" sv-ng-string></div>
<div *ngIf="model.renderedUnRankingChoices.length === 0" [class]="model.cssClasses.containerPlaceholder" [model]="$any(model).locSelectToRankEmptyRankedAreaText" sv-ng-string></div>
</div>

<div [class]="model.cssClasses.containersDivider"></div>

<div [class]='model.getContainerClasses("to")' data-ranking="to-container">
<ng-container *ngFor="let item of model.rankingChoices; index as index; trackBy: trackItemBy">
<ng-container *ngFor="let item of model.renderedRankingChoices; index as index; trackBy: trackItemBy">
<ng-template [component]="{ name: getItemValueComponentName(item), data: getItemValueComponentData(item, index) }"></ng-template>
</ng-container>

<div *ngIf="model.rankingChoices.length === 0" [class]="model.cssClasses.containerPlaceholder" [model]="$any(model).locSelectToRankEmptyUnrankedAreaText" sv-ng-string></div>
<div *ngIf="model.renderedRankingChoices.length === 0" [class]="model.cssClasses.containerPlaceholder" [model]="$any(model).locSelectToRankEmptyUnrankedAreaText" sv-ng-string></div>
</div>
</div>
2 changes: 1 addition & 1 deletion packages/survey-vue3-ui/src/Ranking.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div :class="question.rootClass" ref="root">
<template v-if="!question.selectToRankEnabled">
<component
v-for="(item, index) in question.rankingChoices"
v-for="(item, index) in question.renderedRankingChoices"
:key="item.value + '-' + index + '-item'"
:is="getItemValueComponentName(item)"
v-bind="getItemValueComponentData(item, index)"
Expand Down
1 change: 1 addition & 0 deletions src/defaultCss/defaultV2Css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export var defaultV2Css = {
rowMultiple: "sd-row--multiple",
rowCompact: "sd-row--compact",
rowFadeIn: "sd-row--fade-in",
rowDelayedFadeIn: "sd-row--delayed-fade-in",
rowFadeOut: "sd-row--fade-out",
pageRow: "sd-page__row",
question: {
Expand Down
14 changes: 13 additions & 1 deletion src/defaultV2-theme/blocks/sd-row.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@
margin-top: var(--sd-base-vertical-padding);
--animate-margin: var(--sd-base-vertical-padding);
}
& ~ .sd-page__row.sd-row.sd-row--fade-in.sd-row--fade-in {
margin-top: 0;
}
}

.sd-row.sd-page__row:not(.sd-row--compact) {
& ~ .sd-row.sd-page__row:not(.sd-row--compact) {
margin-top: calcSize(2);
--animate-margin: #{calcSize(2)};
}
& ~ .sd-page__row.sd-row.sd-row--fade-in.sd-row--fade-in {
margin-top: 0;
}
}

.sd-row--multiple {
Expand Down Expand Up @@ -112,16 +118,22 @@
margin-top: var(--animate-margin);
}
}

.sd-row.sd-row--fade-in {
margin-top: 0;
}
.sd-row--fade-in {
animation-fill-mode: forwards;
animation-name: fadeIn, moveInWithOverflow, marginFadeIn;
min-height: 0 !important;
opacity: 0;
height: 0;
animation-timing-function: $ease-out;
animation-delay: $row-fade-in-delay, 0s, 0s;
animation-duration: $row-fade-in-duration, $row-move-in-duration, $row-move-in-duration;
}
.sd-row--delayed-fade-in {
animation-delay: calc(#{$row-fade-in-delay} + #{$row-fade-in-animation-delay}), $row-fade-in-animation-delay, $row-fade-in-animation-delay;
}

.sd-row--fade-out {
animation-name: fadeIn, moveInWithOverflow, marginFadeIn;
Expand Down
1 change: 1 addition & 0 deletions src/defaultV2-theme/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ $row-fade-in-delay: var(--sjs-row-fade-in-delay, 150ms);
$row-fade-out-duration: var(--sjs-row-fade-out-duration, 150ms);
$row-move-out-duration: var(--sjs-row-move-out-duration, 250ms);
$row-move-out-delay: var(--sjs-row-move-out-delay, 100ms);
$row-fade-in-animation-delay: var(--sjs-row-fade-in-animation-delay, 400ms);

$expand-fade-in-duration: var(--sjs-expand-fade-in-duration, 500ms);
$expand-move-in-duration: var(--sjs-expand-move-in-duration, 150ms);
Expand Down
90 changes: 43 additions & 47 deletions src/dragdrop/ranking-choices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DragDropChoices } from "./choices";
import { CssClassBuilder } from "../utils/cssClassBuilder";
import { IsMobile } from "../utils/devices";
import { DomDocumentHelper } from "../global_variables_utils";
import { QuestionRankingModel } from "../question_ranking";
export class DragDropRankingChoices extends DragDropChoices {
protected get draggedElementType(): string {
return "ranking-item";
Expand Down Expand Up @@ -63,72 +64,67 @@ export class DragDropRankingChoices extends DragDropChoices {
dropTargetNode?: HTMLElement
): boolean {
const choices = this.parentElement.rankingChoices;

const dropTargetIndex = choices.indexOf(this.dropTarget);
const draggedElementIndex = choices.indexOf(this.draggedElement);

if (draggedElementIndex > dropTargetIndex && dropTargetNode.classList.contains("sv-dragdrop-moveup")) {
this.parentElement.dropTargetNodeMove = null;
return false;
}

if (draggedElementIndex < dropTargetIndex && dropTargetNode.classList.contains("sv-dragdrop-movedown")) {
this.parentElement.dropTargetNodeMove = null;
return false;
}

if (choices.indexOf(dropTarget) === -1)
// shouldn't allow to drop on "adorners" (selectall, none, other)
return false;

return true;
}
protected calculateIsBottom(clientY: number): boolean {
const choices = this.parentElement.rankingChoices;
return (
choices.indexOf(this.dropTarget) - choices.indexOf(this.draggedElement) >
0
);
protected calculateIsBottom(clientY: number, dropTargetNode?: HTMLElement): boolean {
if(this.dropTarget instanceof ItemValue && this.draggedElement !== this.dropTarget) {
const rect = dropTargetNode.getBoundingClientRect();
return clientY >= rect.y + rect.height / 2;
}
return super.calculateIsBottom(clientY);
}

protected doDragOver = (): any => {
const node = this.domAdapter.draggedElementShortcut.querySelector<HTMLElement>(".sv-ranking-item");
node.style.cursor = "grabbing";
};
public getIndixies(model: any, fromChoicesArray: Array<ItemValue>, toChoicesArray: Array<ItemValue>) {
let fromIndex = fromChoicesArray.indexOf(this.draggedElement);
let toIndex = toChoicesArray.indexOf(this.dropTarget);

if (toIndex === -1) {
const length = model.value.length;
toIndex = length;
} else if(fromChoicesArray == toChoicesArray) {
if(!this.isBottom && fromIndex < toIndex) toIndex--;
if(this.isBottom && fromIndex > toIndex) toIndex ++;
} else if(fromChoicesArray != toChoicesArray) {
if(this.isBottom) toIndex++;
}

return { fromIndex, toIndex };
}

protected afterDragOver(dropTargetNode: HTMLElement): void {
const choices = this.parentElement.rankingChoices;
const dropTargetIndex = choices.indexOf(this.dropTarget);
const draggedElementIndex = choices.indexOf(this.draggedElement);

choices.splice(draggedElementIndex, 1);
choices.splice(dropTargetIndex, 0, this.draggedElement);
this.parentElement.setPropertyValue("rankingChoices", choices);
//return;
this.updateDraggedElementShortcut(dropTargetIndex + 1);

if (draggedElementIndex !== dropTargetIndex) {
dropTargetNode.classList.remove("sv-dragdrop-moveup");
dropTargetNode.classList.remove("sv-dragdrop-movedown");
this.parentElement.dropTargetNodeMove = null;
}
const { fromIndex, toIndex } = this.getIndixies(this.parentElement, this.parentElement.rankingChoices, this.parentElement.rankingChoices);
this.reorderRankedItem(this.parentElement as QuestionRankingModel, fromIndex, toIndex);
}

if (draggedElementIndex > dropTargetIndex) {
this.parentElement.dropTargetNodeMove = "down";
}
public reorderRankedItem = (questionModel: QuestionRankingModel, fromIndex: number, toIndex: number): void => {
if(fromIndex == toIndex) return;
const rankingChoices = questionModel.rankingChoices;
const item = rankingChoices[fromIndex];
questionModel.isValueSetByUser = true;

if (draggedElementIndex < dropTargetIndex) {
this.parentElement.dropTargetNodeMove = "up";
}
rankingChoices.splice(fromIndex, 1);
rankingChoices.splice(toIndex, 0, item);

this.updateDraggedElementShortcut(toIndex + 1);
}

protected updateDraggedElementShortcut(newIndex: number) {
const newIndexText = newIndex !== null ? newIndex + "" : "";
// TODO should avoid direct DOM manipulation, do through the frameworks instead
const indexNode: HTMLElement = this.domAdapter.draggedElementShortcut.querySelector(
".sv-ranking-item__index"
);
indexNode.innerText = newIndexText;
if(this.domAdapter?.draggedElementShortcut) {
const newIndexText = newIndex !== null ? newIndex + "" : "";
// TODO should avoid direct DOM manipulation, do through the frameworks instead
const indexNode: HTMLElement = this.domAdapter.draggedElementShortcut.querySelector(
".sv-ranking-item__index"
);
indexNode.innerText = newIndexText;
}
}

protected ghostPositionChanged(): void {
Expand Down
74 changes: 0 additions & 74 deletions src/dragdrop/ranking-select-to-rank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,53 +83,6 @@ export class DragDropRankingSelectToRank extends DragDropRankingChoices {
rankFunction(questionModel, fromIndex, toIndex, dropTargetNode);
}

public getIndixies(model: any, fromChoicesArray: Array<ItemValue>, toChoicesArray: Array<ItemValue>) {
let fromIndex = fromChoicesArray.indexOf(this.draggedElement);
let toIndex = toChoicesArray.indexOf(this.dropTarget);

if (toIndex === -1) {
const length = model.value.length;
toIndex = length;
} else if(fromChoicesArray == toChoicesArray) {
if(!this.isBottom && fromIndex < toIndex) toIndex--;
if(this.isBottom && fromIndex > toIndex) toIndex ++;
} else if(fromChoicesArray != toChoicesArray) {
if(this.isBottom) toIndex++;
}

return { fromIndex, toIndex };
}
protected calculateIsBottom(clientY: number, dropTargetNode?: HTMLElement): boolean {
if(this.dropTarget instanceof ItemValue && this.draggedElement !== this.dropTarget) {
const rect = dropTargetNode.getBoundingClientRect();
return clientY >= rect.y + rect.height / 2;
}
return super.calculateIsBottom(clientY);
}

private doUIEffects(dropTargetNode: HTMLElement, fromIndex: number, toIndex: number) {
const questionModel: any = this.parentElement;
const isDropToEmptyRankedContainer = this.dropTarget === "to-container" && questionModel.isEmpty();
const isNeedToShowIndexAtShortcut = !this.isDropTargetUnranked || isDropToEmptyRankedContainer;
const shortcutIndex = isNeedToShowIndexAtShortcut ? toIndex + 1 : null;

this.updateDraggedElementShortcut(shortcutIndex);

if (fromIndex !== toIndex) {
dropTargetNode.classList.remove("sv-dragdrop-moveup");
dropTargetNode.classList.remove("sv-dragdrop-movedown");
questionModel.dropTargetNodeMove = null;
}

if (fromIndex > toIndex) {
questionModel.dropTargetNodeMove = "down";
}

if (fromIndex < toIndex) {
questionModel.dropTargetNodeMove = "up";
}
}

private get isDraggedElementRanked() {
return this.parentElement.rankingChoices.indexOf(this.draggedElement) !== -1;
}
Expand All @@ -143,10 +96,6 @@ export class DragDropRankingSelectToRank extends DragDropRankingChoices {
return !this.isDraggedElementRanked;
}

private get isDropTargetUnranked() {
return !this.isDropTargetRanked;
}

private updateChoices(questionModel: QuestionRankingModel, rankingChoices: Array<ItemValue>) {
questionModel.isValueSetByUser = true;
questionModel.rankingChoices = rankingChoices;
Expand All @@ -166,27 +115,4 @@ export class DragDropRankingSelectToRank extends DragDropRankingChoices {
rankingChoices.splice(fromIndex, 1);
this.updateChoices(questionModel, rankingChoices);
}

public reorderRankedItem = (questionModel: QuestionRankingModel, fromIndex: number, toIndex: number, dropTargetNode?: HTMLElement): void => {
const rankingChoices = questionModel.rankingChoices;
const item = rankingChoices[fromIndex];
if(fromIndex == toIndex) return;

questionModel.isValueSetByUser = true;
rankingChoices.splice(fromIndex, 1);
rankingChoices.splice(toIndex, 0, item);
questionModel.setPropertyValue("rankingChoices", rankingChoices);
if(dropTargetNode) {
this.doUIEffects(dropTargetNode, fromIndex, toIndex);
}

}
public clear(): void {
const questionModel = <QuestionRankingModel>this.parentElement;
if(!!questionModel) {
questionModel.rankingChoicesAnimation.cancel();
questionModel.unRankingChoicesAnimation.cancel();
}
super.clear();
}
}
10 changes: 5 additions & 5 deletions src/knockout/templates/question-ranking.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script type="text/html" id="survey-question-ranking">
<!-- ko ifnot: question.selectToRankEnabled -->
<div data-bind="css: question.rootClass">
<!-- ko foreach: { data: question.rankingChoices, as: 'item', afterRender: question.koAfterRender } -->
<!-- ko foreach: { data: question.renderedRankingChoices, as: 'item', afterRender: question.koAfterRender } -->
<!-- ko component: { name: question.getItemValueWrapperComponentName(item), params: { number: question.getNumberByIndex($index()), componentData: question.getItemValueWrapperComponentData(item), templateData: { name: 'survey-ranking-item', data: item } } } -->
<!-- /ko -->
<!-- /ko -->
Expand All @@ -11,11 +11,11 @@
<!-- ko if: question.selectToRankEnabled -->
<div data-bind="css: question.rootClass">
<div data-bind="css: question.getContainerClasses('from')" data-ranking="from-container">
<!-- ko foreach: { data: question.unRankingChoices, as: 'item', afterRender: question.koAfterRender } -->
<!-- ko foreach: { data: question.renderedUnRankingChoices, as: 'item', afterRender: question.koAfterRender } -->
<!-- ko component: { name: question.getItemValueWrapperComponentName(item), params: { number: '', componentData: question.getItemValueWrapperComponentData(item), templateData: { name: 'survey-ranking-item', data: item } } } -->
<!-- /ko -->
<!-- /ko -->
<!-- ko if: question.unRankingChoices.length === 0 -->
<!-- ko if: question.renderedUnRankingChoices.length === 0 -->
<div data-bind="css: cssClasses.containerPlaceholder">
<!-- ko template: { name: 'survey-string', data: question.locSelectToRankEmptyRankedAreaText } -->
<!-- /ko -->
Expand All @@ -24,11 +24,11 @@
</div>
<div data-bind="css: cssClasses.containersDivider"></div>
<div data-bind="css: question.getContainerClasses('to')" data-ranking="to-container">
<!-- ko foreach: { data: question.rankingChoices, as: 'item', afterRender: question.koAfterRender } -->
<!-- ko foreach: { data: question.renderedRankingChoices, as: 'item', afterRender: question.koAfterRender } -->
<!-- ko component: { name: question.getItemValueWrapperComponentName(item), params: { number: question.getNumberByIndex($index()), componentData: question.getItemValueWrapperComponentData(item), templateData: { name: 'survey-ranking-item', data: item } } } -->
<!-- /ko -->
<!-- /ko -->
<!-- ko if: question.rankingChoices.length === 0 -->
<!-- ko if: question.renderedRankingChoices.length === 0 -->
<div data-bind="css: cssClasses.containerPlaceholder">
<!-- ko template: { name: 'survey-string', data: question.locSelectToRankEmptyUnrankedAreaText } -->
<!-- /ko -->
Expand Down
8 changes: 4 additions & 4 deletions src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class PageModel extends PanelModelBase implements IPage {
}
public get isStarted(): boolean { return this.isStartPage; }
protected calcCssClasses(css: any): any {
const classes = { page: {}, error: {}, pageTitle: "", pageDescription: "", row: "", rowMultiple: "", pageRow: "", rowCompact: "", rowFadeIn: "", rowFadeOut: "", rowFadeOutActive: "" };
const classes = { page: {}, error: {}, pageTitle: "", pageDescription: "", row: "", rowMultiple: "", pageRow: "", rowCompact: "", rowFadeIn: "", rowFadeOut: "", rowDelayedFadeIn: "" };
this.copyCssClasses(classes.page, css.page);
this.copyCssClasses(classes.error, css.error);
if (!!css.pageTitle) {
Expand All @@ -138,12 +138,12 @@ export class PageModel extends PanelModelBase implements IPage {
if (!!css.rowFadeIn) {
classes.rowFadeIn = css.rowFadeIn;
}
if (!!css.rowDelayedFadeIn) {
classes.rowDelayedFadeIn = css.rowDelayedFadeIn;
}
if (!!css.rowFadeOut) {
classes.rowFadeOut = css.rowFadeOut;
}
if (!!css.rowFadeOutActive) {
classes.rowFadeOutActive = css.rowFadeOutActive;
}
if (this.survey) {
this.survey.updatePageCssClasses(this, classes);
}
Expand Down
Loading

0 comments on commit 2d9c7a8

Please sign in to comment.