diff --git a/v3/src/components/slider/slider-model.ts b/v3/src/components/slider/slider-model.ts index de33f5c57..46c4dd85e 100644 --- a/v3/src/components/slider/slider-model.ts +++ b/v3/src/components/slider/slider-model.ts @@ -8,9 +8,11 @@ import { getGlobalValueManager, getSharedModelManager } from "../../models/tiles import { ITileContentModel, TileContentModel } from "../../models/tiles/tile-content" import { DateUnit, dateUnits, unitsStringToMilliseconds } from "../../utilities/date-utils" import { kSliderTileType } from "./slider-defs" -import {AnimationDirection, AnimationDirections, AnimationMode, AnimationModes, FixValueFn, ISliderScaleType, +import { + AnimationDirection, AnimationDirections, AnimationMode, AnimationModes, FixValueFn, ISliderScaleType, kDefaultAnimationDirection, kDefaultAnimationMode, kDefaultAnimationRate, kDefaultDateMultipleOfUnit, - kDefaultSliderScaleType, SliderScaleTypes} from "./slider-types" + kDefaultSliderAxisMax, kDefaultSliderAxisMin, kDefaultSliderScaleType, SliderScaleTypes +} from "./slider-types" export const SliderModel = TileContentModel .named("SliderModel") @@ -25,7 +27,7 @@ export const SliderModel = TileContentModel _animationRate: types.maybe(types.number), // frames per second scaleType: types.optional(types.enumeration([...SliderScaleTypes]), kDefaultSliderScaleType), axis: types.optional(types.union(NumericAxisModel, DateAxisModel), - () => NumericAxisModel.create({ place: 'bottom', min: -0.5, max: 11.5 })) + () => NumericAxisModel.create({ place: 'bottom', min: kDefaultSliderAxisMin, max: kDefaultSliderAxisMax })) }) .views(self => ({ get name() { @@ -169,7 +171,8 @@ export const SliderModel = TileContentModel if (scaleType !== self.scaleType) { switch (scaleType) { case "numeric": - self.axis = NumericAxisModel.create({ place: 'bottom', min: -0.5, max: 11.5 }) + self.axis = NumericAxisModel.create({ + place: 'bottom', min: kDefaultSliderAxisMin, max: kDefaultSliderAxisMax }) self.setValue(0.5) break case "date": { diff --git a/v3/src/components/slider/slider-registration.test.ts b/v3/src/components/slider/slider-registration.test.ts index 97b149226..b95b077be 100644 --- a/v3/src/components/slider/slider-registration.test.ts +++ b/v3/src/components/slider/slider-registration.test.ts @@ -85,6 +85,7 @@ describe("Slider registration", () => { }) expect(tileWithNoSharedModel).toBeUndefined() + // export numeric slider in v2 format const row = docContent.getRowByIndex(0) as IFreeTileRow const sliderExport = exportV2Component({ tile, row, sharedModelManager }) expect(sliderExport?.type).toBe("DG.SliderView") @@ -94,5 +95,19 @@ describe("Slider registration", () => { expect(sliderStorage.animationMode).toBe(1) expect(sliderStorage.maxPerSecond).toBeNull() expect(sliderStorage.restrictToMultiplesOf).toBeNull() + + // change to date-time slider and export in v2 format + sliderModel!.setScaleType("date") + sliderModel!.setDateMultipleOfUnit("day") + const dateSliderExport = exportV2Component({ tile, row, sharedModelManager }) + expect(dateSliderExport?.type).toBe("DG.SliderView") + const dateSliderStorage = dateSliderExport!.componentStorage as ICodapV2SliderStorage + expect(dateSliderStorage._links_?.model).toBeDefined() + expect(dateSliderStorage.animationDirection).toBe(1) + expect(dateSliderStorage.animationMode).toBe(1) + expect(dateSliderStorage.maxPerSecond).toBeNull() + expect(dateSliderStorage.restrictToMultiplesOf).toBeNull() + expect(dateSliderStorage.v3?.scaleType).toBe("date") + expect(dateSliderStorage.v3?.dateMultipleOfUnit).toBe("day") }) }) diff --git a/v3/src/components/slider/slider-registration.ts b/v3/src/components/slider/slider-registration.ts index a3c7c3c24..869631008 100644 --- a/v3/src/components/slider/slider-registration.ts +++ b/v3/src/components/slider/slider-registration.ts @@ -10,6 +10,7 @@ import { ITileLikeModel, registerTileContentInfo } from "../../models/tiles/tile import { getGlobalValueManager } from "../../models/tiles/tile-environment" import { ITileModelSnapshotIn } from "../../models/tiles/tile-model" import { toV2Id, toV3GlobalId, toV3Id } from "../../utilities/codap-utils" +import { DateUnit, unitsStringToMilliseconds } from "../../utilities/date-utils" import { isFiniteNumber } from "../../utilities/math-utils" import { isAliveSafe } from "../../utilities/mst-utils" import { t } from "../../utilities/translation/translate" @@ -25,7 +26,7 @@ import { ISliderSnapshot, SliderModel, isSliderModel } from "./slider-model" import { SliderTitleBar } from "./slider-title-bar" import { AnimationDirection, AnimationDirections, AnimationMode, AnimationModes, - kDefaultAnimationDirection, kDefaultAnimationMode + kDefaultAnimationDirection, kDefaultAnimationMode, kDefaultSliderAxisMax, kDefaultSliderAxisMin } from "./slider-types" import { kDefaultSliderName, kDefaultSliderValue } from "./slider-utils" @@ -74,7 +75,7 @@ registerTileComponentInfo({ defaultHeight: 73 }) -registerV2TileExporter((kSliderTileType), ({ tile }) => { +registerV2TileExporter(kSliderTileType, ({ tile }) => { const sliderModel = isSliderModel(tile.content) ? tile.content : undefined if (!sliderModel) return const { @@ -82,7 +83,9 @@ registerV2TileExporter((kSliderTileType), ({ tile }) => { animationDirection, animationMode, _animationRate, - multipleOf + multipleOf, + dateMultipleOfUnit, + scaleType } = sliderModel const domain = isFiniteNumber(lowerBound) && isFiniteNumber(upperBound) ? { lowerBound, upperBound } : undefined @@ -93,6 +96,12 @@ registerV2TileExporter((kSliderTileType), ({ tile }) => { const getAnimationModeIndex = (mode?: AnimationMode) => { return mode != null ? AnimationModes.findIndex(_mode => _mode === mode) : 1 } + // v2 doesn't support date-time sliders; convert to seconds instead + const restrictToMultiplesOf = scaleType === "date" && multipleOf != null + ? multipleOf * unitsStringToMilliseconds(dateMultipleOfUnit) / 1000 + : multipleOf + // v3 extensions: ignored by v2, but allows full round-trip for v3 save/restore + const v3: ICodapV2SliderStorage["v3"] = { scaleType, multipleOf, dateMultipleOfUnit } const componentStorage: SetOptional = { _links_: { model: guidLink("DG.GlobalValue", toV2Id(tile.id)) }, @@ -100,7 +109,8 @@ registerV2TileExporter((kSliderTileType), ({ tile }) => { animationDirection: getAnimationDirectionIndex(animationDirection), animationMode: getAnimationModeIndex(animationMode), maxPerSecond: _animationRate ?? null, - restrictToMultiplesOf: multipleOf ?? null + restrictToMultiplesOf: restrictToMultiplesOf ?? null, + v3 } return { type: "DG.SliderView", componentStorage } }) @@ -116,7 +126,7 @@ registerV2TileImporter("DG.SliderView", ({ v2Component, v2Document, sharedModelM guid: componentGuid, componentStorage: { name, title: v2Title = "", _links_, lowerBound, upperBound, animationDirection, animationMode, - restrictToMultiplesOf, maxPerSecond, userTitle, userSetTitle + restrictToMultiplesOf, maxPerSecond, userTitle, userSetTitle, v3 } } = v2Component const globalId = _links_.model.id @@ -137,15 +147,20 @@ registerV2TileImporter("DG.SliderView", ({ v2Component, v2Document, sharedModelM return AnimationModes[mode] || kDefaultAnimationMode } + const axisType = v3?.scaleType ?? "numeric" + const axisMin = lowerBound ?? kDefaultSliderAxisMin + const axisMax = upperBound ?? kDefaultSliderAxisMax + // create slider model const content: ISliderSnapshot = { type: kSliderTileType, globalValue: globalValue.id, - multipleOf: restrictToMultiplesOf ?? undefined, + multipleOf: v3?.multipleOf ?? restrictToMultiplesOf ?? undefined, + dateMultipleOfUnit: v3?.dateMultipleOfUnit as DateUnit ?? undefined, animationDirection: getAnimationDirectionStr(animationDirection), animationMode: getAnimationModeStr(animationMode), _animationRate: maxPerSecond ?? undefined, - axis: { type: "numeric", place: "bottom", min: lowerBound ?? 0, max: upperBound ?? 12 } + axis: { type: axisType, place: "bottom", min: axisMin, max: axisMax } } const title = v2Title && (userTitle || userSetTitle) ? v2Title : undefined const sliderTileSnap: ITileModelSnapshotIn = { diff --git a/v3/src/components/slider/slider-types.ts b/v3/src/components/slider/slider-types.ts index daef3fcdb..b07d1c99a 100644 --- a/v3/src/components/slider/slider-types.ts +++ b/v3/src/components/slider/slider-types.ts @@ -9,6 +9,9 @@ export const kDefaultSliderAxisTop = 0 export const kDefaultSliderAxisHeight = 24 export const kDefaultSliderPadding = 10 +export const kDefaultSliderAxisMin = -0.5 +export const kDefaultSliderAxisMax = 11.5 + // values are translation string keys; indices are v2 values export const AnimationDirections = ["backAndForth", "lowToHigh", "highToLow"] as const export type AnimationDirection = typeof AnimationDirections[number] diff --git a/v3/src/v2/codap-v2-types.ts b/v3/src/v2/codap-v2-types.ts index f6bae635f..43a7b9c5d 100644 --- a/v3/src/v2/codap-v2-types.ts +++ b/v3/src/v2/codap-v2-types.ts @@ -245,6 +245,8 @@ export interface ICodapV2BaseComponentStorage { // In the CFM shared files there are more than 20,000 examples of cannotClose: true // and more 20,000 examples cannotClose: false cannotClose?: boolean + // allows v2 documents saved by v3 to contain v3-specific enhancements + v3?: object } export interface ICodapV2CalculatorStorage extends ICodapV2BaseComponentStorage { @@ -264,6 +266,12 @@ export interface ICodapV2SliderStorage extends ICodapV2BaseComponentStorage { // NOTE: v2 writes out the `userTitle` property, but reads in property `userChangedTitle`. // It is also redundant with the `userSetTitle` property shared by all components. ¯\_(ツ)_/¯ userTitle?: boolean + // v3 enhancements + v3?: { + scaleType: "numeric" | "date", + multipleOf?: number + dateMultipleOfUnit?: string + } } export interface ICodapV2RowHeight {