diff --git a/cypress/e2e/AssignedActivity/multipageActivities.cy.js b/cypress/e2e/AssignedActivity/multipageActivities.cy.js
index c454419c38..3ad123f650 100644
--- a/cypress/e2e/AssignedActivity/multipageActivities.cy.js
+++ b/cypress/e2e/AssignedActivity/multipageActivities.cy.js
@@ -184,7 +184,7 @@ describe('Multipage activity tests', function () {
cy.get('[data-test="View Activity"]').click();
cy.get('#page1\\/top').should('contain.text', 'top 1')
- cy.get('#page2\\/top').should('contain.text', 'top 2')
+ cy.get('#page2\\/top').should('contain.text', 'top 2')
cy.url().should('match', /#page1$/)
@@ -2350,7 +2350,7 @@ describe('Multipage activity tests', function () {
cy.get('.navigationRow').eq(0).find('.navigationColumn1').click();
cy.get('[data-test="Assign Activity"]').click();
cy.wait(1500); // wait for update
-
+
cy.get('[data-test="RoleDropDown"] > div:nth-child(2)').click().type("{downArrow}{downArrow}{enter}")
@@ -2559,5 +2559,83 @@ describe('Multipage activity tests', function () {
})
+ it('Change pages with navigateToTarget action and choiceinput', () => {
+ const doenetML1 = `
+
+
+
+
+
+
+
Page 1
+
+
+ Page 1
+ Page 2
+ Page 3
+
+
+
+
+
+ `
+
+ const doenetML2 = `Page 2`
+ const doenetML3 = `Page 3`
+
+ cy.createMultipageActivity({ courseId, doenetId, parentDoenetId: courseId, pageDoenetId1, pageDoenetId2, pageDoenetId3, doenetML1, doenetML2, doenetML3 });
+
+ cy.visit(`http://localhost/course?tool=navigation&courseId=${courseId}`)
+
+ cy.get('.navigationRow').should('have.length', 1); //Need this to wait for the row to appear
+ cy.get('.navigationRow').eq(0).get('.navigationColumn1').click();
+
+ cy.get('[data-test="Assign Activity"]').click();
+ cy.get('[data-test="Unassign Activity"]').should('be.visible')
+
+ cy.get('[data-test="View Assigned Activity"]').click();
+
+ cy.get('#page1').should('contain.text', 'Page 1')
+
+ cy.url().should('match', /#page1$/)
+
+
+ cy.get(`#page1\\/moveToPage`).select('2');
+
+ cy.get('#page2').should('contain.text', 'Page 2')
+
+ cy.url().should('match', /#page2$/)
+
+
+ cy.get('[data-test=previous]').click();
+
+ cy.get('#page1').should('contain.text', 'Page 1')
+
+ cy.url().should('match', /#page1$/)
+
+
+ cy.get(`#page1\\/moveToPage`).select('3');
+
+ cy.get('#page3').should('contain.text', 'Page 3')
+
+ cy.url().should('match', /#page3$/)
+
+
+ cy.get('[data-test=previous]').click();
+
+ cy.get('#page2').should('contain.text', 'Page 2')
+
+ cy.url().should('match', /#page2$/)
+
+
+ cy.get('[data-test=previous]').click();
+
+ cy.get('#page1').should('contain.text', 'Page 1')
+
+ cy.url().should('match', /#page1$/)
+
+
+
+ })
})
\ No newline at end of file
diff --git a/cypress/e2e/DoenetML/tagSpecific/ref.cy.js b/cypress/e2e/DoenetML/tagSpecific/ref.cy.js
index 335c1ab3cd..5813f86276 100644
--- a/cypress/e2e/DoenetML/tagSpecific/ref.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/ref.cy.js
@@ -295,5 +295,94 @@ describe('ref Tag Tests', function () {
});
+ it('navigate to target action opens aside', () => {
+ cy.window().then(async (win) => {
+ win.postMessage({
+ doenetML: `
+ [Aside](/aside)
+
+
+
+
+
+
+
+
+
+ `}, "*");
+ });
+
+ // to wait for page to load
+ cy.get('#\\/asideTitle').should('have.text', 'The aside')
+
+ cy.log('Aside closed at the beginning')
+ cy.get('#\\/inside').should('not.exist')
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/aside"].stateValues.open).eq(false);
+ })
+
+ cy.log('clicking action opens aside')
+ cy.get('#\\/go').click();
+
+ cy.get('#\\/inside').should('have.text', "Inside the aside");
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/aside"].stateValues.open).eq(true);
+ })
+
+ });
+
+ it('chain action to navigate to target', () => {
+ cy.window().then(async (win) => {
+ win.postMessage({
+ doenetML: `
+
+
+
+
+
+
+
+
+
+ [Aside](/aside)
+
+
+
+
+
+
+
+
+
+
+ `}, "*");
+ });
+
+ // to wait for page to load
+ cy.get('#\\/asideTitle').should('have.text', 'Counting')
+
+ cy.log('Aside closed at the beginning')
+ cy.get('#\\/n').should('not.exist')
+
+ cy.log('clicking action opens aside and starts counting')
+ cy.get('#\\/startCount').click();
+
+ cy.get('#\\/n').should('have.text', '1');
+ cy.get('#\\/n').should('have.text', '2');
+ cy.get('#\\/n').should('have.text', '3');
+ cy.get('#\\/n').should('have.text', '4');
+ cy.get('#\\/n').should('have.text', '5');
+
+ });
+
+
});
\ No newline at end of file
diff --git a/src/Core/Core.js b/src/Core/Core.js
index 2278fbabc4..4fe224f9cd 100644
--- a/src/Core/Core.js
+++ b/src/Core/Core.js
@@ -74,6 +74,7 @@ export default class Core {
recordSolutionView: this.recordSolutionView.bind(this),
requestComponentDoenetML: this.requestComponentDoenetML.bind(this),
copyToClipboard: this.copyToClipboard.bind(this),
+ navigateToTarget: this.navigateToTarget.bind(this),
}
this.updateInfo = {
@@ -10445,6 +10446,14 @@ export default class Core {
})
}
}
+
+ navigateToTarget(args) {
+ postMessage({
+ messageType: "navigateToTarget",
+ coreId: this.coreId,
+ args
+ })
+ }
}
diff --git a/src/Core/components/Ref.js b/src/Core/components/Ref.js
index 3f9ae324ac..9c0116511b 100644
--- a/src/Core/components/Ref.js
+++ b/src/Core/components/Ref.js
@@ -1,6 +1,14 @@
import InlineComponent from './abstract/InlineComponent';
export default class Ref extends InlineComponent {
+ constructor(args) {
+ super(args);
+
+ Object.assign(this.actions, {
+ navigateToTarget: this.navigateToTarget.bind(this)
+ });
+
+ }
static componentType = "ref";
static renderChildren = true;
@@ -292,4 +300,31 @@ export default class Ref extends InlineComponent {
}
+ async navigateToTarget({ actionId }) {
+
+ if (await this.stateValues.disabled) {
+ this.coreFunctions.resolveAction({ actionId });
+ } else {
+ let cid = await this.stateValues.cid;
+ let doenetId = await this.stateValues.doenetId;
+ let variantIndex = await this.stateValues.variantIndex;
+ let edit = await this.stateValues.edit;
+ let hash = await this.stateValues.hash;
+ let page = await this.stateValues.page;
+ let uri = await this.stateValues.uri;
+ let targetName = await this.stateValues.targetName;
+
+ let effectiveName = this.componentOrAdaptedName
+
+
+ this.coreFunctions.navigateToTarget({
+ cid, doenetId, variantIndex, edit, hash, page, uri, targetName,
+ actionId, componentName: this.componentName, effectiveName
+ })
+ }
+
+
+ }
+
+
}
\ No newline at end of file
diff --git a/src/Viewer/PageViewer.jsx b/src/Viewer/PageViewer.jsx
index fda1aa5411..7c76698fc0 100644
--- a/src/Viewer/PageViewer.jsx
+++ b/src/Viewer/PageViewer.jsx
@@ -5,13 +5,16 @@ import { serializedComponentsReplacer, serializedComponentsReviver } from '../Co
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import { rendererState } from './renderers/useDoenetRenderer';
-import { atom, atomFamily, useRecoilCallback } from 'recoil';
+import { atom, atomFamily, useRecoilCallback, useRecoilValue } from 'recoil';
import { get as idb_get, set as idb_set } from 'idb-keyval';
import { cidFromText } from '../Core/utils/cid';
import { retrieveTextFileForCid } from '../Core/utils/retrieveTextFile';
import axios from 'axios';
import { returnAllPossibleVariants } from '../Core/utils/returnAllPossibleVariants';
-import { useLocation } from "react-router";
+import { useLocation, useNavigate } from "react-router";
+import { pageToolViewAtom } from '../Tools/_framework/NewToolRoot';
+import { itemByDoenetId } from '../_reactComponents/Course/CourseActions';
+
import cssesc from 'cssesc';
const rendererUpdatesToIgnore = atomFamily({
@@ -140,6 +143,13 @@ export default function PageViewer(props) {
const previousLocationKeys = useRef([]);
+ const pageToolView = useRecoilValue(pageToolViewAtom);
+ const itemInCourse = useRecoilValue(itemByDoenetId(props.doenetId));
+ const scrollableContainer = useRecoilValue(scrollableContainerAtom);
+
+ let navigate = useNavigate();
+
+
let location = useLocation();
let hash = location.hash;
@@ -194,6 +204,8 @@ export default function PageViewer(props) {
resetPage(e.data.args);
} else if (e.data.messageType === "copyToClipboard") {
copyToClipboard(e.data.args);
+ } else if (e.data.messageType === "navigateToTarget") {
+ navigateToTarget(e.data.args);
} else if (e.data.messageType === "terminated") {
terminateCoreAndAnimations();
}
@@ -890,6 +902,44 @@ export default function PageViewer(props) {
}
+ async function navigateToTarget({ cid, doenetId, variantIndex, edit, hash, page, uri, targetName, actionId, componentName, effectiveName }) {
+
+ let id = prefixForIds + effectiveName;
+ let { targetForATag, url, haveValidTarget, externalUri } = getURLFromRef({
+ cid, doenetId, variantIndex, edit, hash, page,
+ givenUri: uri,
+ targetName,
+ pageToolView,
+ inCourse: Object.keys(itemInCourse).length > 0,
+ search: location.search,
+ id
+ });
+
+
+ if (haveValidTarget) {
+
+ if (targetForATag === "_blank") {
+ window.open(url, targetForATag);
+ } else {
+
+ // TODO: when fix regular ref navigation to scroll back to previous scroll position
+ // when click the back button
+ // add that ability to this navigation as well
+
+ // let scrollAttribute = scrollableContainer === window ? "scrollY" : "scrollTop";
+ // let stateObj = { fromLink: true }
+ // Object.defineProperty(stateObj, 'previousScrollPosition', { get: () => scrollableContainer?.[scrollAttribute], enumerable: true });
+
+ navigate(url)
+ }
+
+
+ }
+
+
+ resolveAction({ actionId });
+ }
+
if (errMsg !== null) {
let errorIcon =
return {errorIcon} {errMsg}
@@ -1062,4 +1112,84 @@ class ErrorBoundary extends React.Component {
}
return this.props.children;
}
-}
\ No newline at end of file
+}
+
+
+export function getURLFromRef({
+ cid, doenetId, variantIndex,
+ edit, hash, page,
+ givenUri,
+ targetName = "",
+ pageToolView = {},
+ inCourse = false,
+ search = "",
+ id = ""
+}) {
+
+ let url = "";
+ let targetForATag = "_blank";
+ let haveValidTarget = false;
+ let externalUri = false;
+ if (cid || doenetId) {
+ if (cid) {
+ url = `cid=${cid}`;
+ } else {
+ url = `doenetId=${doenetId}`;
+ }
+ if (variantIndex) {
+ url += `&variant=${variantIndex}`;
+ }
+
+ let usePublic = false;
+ if (pageToolView.page === "public") {
+ usePublic = true;
+ } else if (!inCourse) {
+ usePublic = true;
+ }
+ if (usePublic) {
+ if (edit === true || edit === null && pageToolView.page === "public" && pageToolView.tool === "editor") {
+ url = `tool=editor&${url}`;
+ }
+ url = `/public?${url}`;
+ } else if (pageToolView.page === "placementexam") {
+ url = `?tool=exam&${url}`;
+ } else {
+ url = `?tool=assignment&${url}`;
+ }
+
+ haveValidTarget = true;
+
+ if (hash) {
+ url += hash;
+ } else {
+ if (page) {
+ url += `#page${page}`;
+ if (targetName) {
+ url += targetName;
+ }
+ } else if (targetName) {
+ url += '#' + targetName;
+ }
+ }
+ } else if (givenUri) {
+ url = givenUri;
+ if (url.substring(0, 8) === "https://" || url.substring(0, 7) === "http://") {
+ haveValidTarget = true;
+ externalUri = true;
+ }
+ } else {
+ url += search;
+
+ if (page) {
+ url += `#page${page}`;
+ } else {
+ let firstSlash = id.indexOf("/");
+ let prefix = id.substring(0, firstSlash);
+ url += "#" + prefix;
+ }
+ url += targetName;
+ targetForATag = null;
+ haveValidTarget = true;
+ }
+ return { targetForATag, url, haveValidTarget, externalUri };
+}
diff --git a/src/Viewer/renderers/ref.jsx b/src/Viewer/renderers/ref.jsx
index c3b34abe10..466029c1ae 100644
--- a/src/Viewer/renderers/ref.jsx
+++ b/src/Viewer/renderers/ref.jsx
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { pageToolViewAtom } from '../../Tools/_framework/NewToolRoot';
import { itemByDoenetId } from '../../_reactComponents/Course/CourseActions';
-import { scrollableContainerAtom } from '../PageViewer';
+import { getURLFromRef, scrollableContainerAtom } from '../PageViewer';
import useDoenetRender from './useDoenetRenderer';
import styled from 'styled-components';
@@ -62,71 +62,15 @@ export default React.memo(function Ref(props) {
linkContent = SVs.linkText;
}
- let url = "";
- let targetForATag = "_blank";
- let haveValidTarget = false;
- let externalUri = false;
- if (SVs.cid || SVs.doenetId) {
- if (SVs.cid) {
- url = `cid=${SVs.cid}`
- } else {
- url = `doenetId=${SVs.doenetId}`
- }
- if (SVs.variantIndex) {
- url += `&variant=${SVs.variantIndex}`;
- }
-
- let usePublic = false;
- if (pageToolView.page === "public") {
- usePublic = true;
- } else if (Object.keys(itemInCourse).length === 0) {
- usePublic = true;
- }
- if (usePublic) {
- if (SVs.edit === true || SVs.edit === null && pageToolView.page === "public" && pageToolView.tool === "editor") {
- url = `tool=editor&${url}`;
- }
- url = `/public?${url}`
- } else if (pageToolView.page === "placementexam") {
- url = `?tool=exam&${url}`
- } else {
- url = `?tool=assignment&${url}`
- }
-
- haveValidTarget = true;
-
- if (SVs.hash) {
- url += SVs.hash;
- } else {
- if (SVs.page) {
- url += `#page${SVs.page}`
- if (SVs.targetName) {
- url += SVs.targetName;
- }
- } else if (SVs.targetName) {
- url += '#' + SVs.targetName;
- }
- }
- } else if (SVs.uri) {
- url = SVs.uri;
- if (url.substring(0, 8) === "https://" || url.substring(0, 7) === "http://") {
- haveValidTarget = true;
- externalUri = true;
- }
- } else {
- url += search;
-
- if (SVs.page) {
- url += `#page${SVs.page}`
- } else {
- let firstSlash = id.indexOf("/");
- let prefix = id.substring(0, firstSlash);
- url += "#" + prefix;
- }
- url += SVs.targetName;
- targetForATag = null;
- haveValidTarget = true;
- }
+ let { targetForATag, url, haveValidTarget, externalUri } = getURLFromRef({
+ cid: SVs.cid, doenetId: SVs.doenetId, variantIndex: SVs.variantIndex,
+ edit: SVs.edit, hash: SVs.hash, page: SVs.page,
+ givenUri: SVs.uri,
+ targetName: SVs.targetName,
+ pageToolView,
+ inCourse: Object.keys(itemInCourse).length > 0,
+ search, id
+ });
if (SVs.createButton) {