Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ stylecop.*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
dist/
dist_local/
/scripts/viewer/index.html
/scripts/guides/default/
userfiles

# Backup & report files from converting an old project file to a newer
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ run the command below in a terminal

This will build a zip in the parent folder of the cloned repo with the naming patern a2j-viewer_{MAJVER}.{MINVER}.{PATCH}_{YYYY}-{MM}-{DD}.zip

## Local Development & Testing

If you need to modify the A2J Viewer source or test it within a local environment, follow the steps below.

### 1. Configuration of Interview Files
Before running the local development server, ensure your interview/guide files are placed in the following directory:
`scripts/guides/default/`

> **Note:** If this directory is empty, the viewer will fallback to the default sample guides located in the `demo` folder.

### 2. Launching the Development Server
To initiate the local build process and start the web server, execute the following command in your terminal:

```bash
npm run dev
```

### 3. Workflow Limitations (No HMR)
Please be advised that Hot Module Replacement (HMR) is currently *not supported*.
To reflect any changes made to the local source files (JS, LESS, or HTML), you must stop the current process and re-run the `npm run dev` command to rebuild the assets.

## Upgrading
1.) backup your old viewer and Guided Interviews

Expand Down
24 changes: 18 additions & 6 deletions demo/build.viewer.html.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
const fs = require('fs')
const path = require('path')

const buildViewerHtml = function () {
const buildViewerHtml = function (isLocal = false) {
const version = Date.now()
const html = template(version)
const templatePath = isLocal ? '.' : '..'
const html = template(version, templatePath)

function template (version) {
function template (version, templatePath) {
return `<!--
A2J Author 7 * Justice * justicia * 正义 * công lý * 사법 * правосудие
All Contents Copyright The Center for Computer-Assisted Legal Instruction
Expand All @@ -31,11 +32,11 @@ const buildViewerHtml = function () {
<script>
localStorage.setItem('a2jConfig', JSON.stringify({
// path (or url) to the interview XML file
templateURL: '../guides/default/Guide.xml',
templateURL: '${templatePath}/guides/default/Guide.xml',

// folder containing images, templates and other assets related to the interview
// path can be relative or fully qualified but requires a trailing slash
fileDataURL: '../guides/default/',
fileDataURL: '${templatePath}/guides/default/',

// endpoint to load an answer file at start, used for RESUME
getDataURL: '',
Expand Down Expand Up @@ -71,7 +72,18 @@ const buildViewerHtml = function () {
</html>`
}

fs.writeFileSync(path.join(__dirname, '/viewer/viewer.html'), html, 'utf-8')
if (isLocal) {
const outputPath = path.join(__dirname, '../scripts/viewer/index.html')
const dirPath = path.dirname(outputPath)

// check folder exists, if not create it
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
fs.writeFileSync(outputPath, html, 'utf-8')
} else {
fs.writeFileSync(path.join(__dirname, '/viewer/viewer.html'), html, 'utf-8')
}
}

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "a2jviewer/app",
"scripts": {
"lint": "standard --fix --env mocha",
"dev": "node scripts/build.local.js npx http-server dist_local && npx http-server dist_local",
"test": "npm run lint && testee --reporter Spec --browsers firefox tests/index.html",
"build": "grunt build --gruntfile=Gruntfile.js",
"deploy": "npm i && npm run build && mv index.html index.dev.html && mv index.production.html index.html",
Expand Down
78 changes: 78 additions & 0 deletions scripts/build.local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env node

const path = require('path')
const fs = require('fs-extra')
const chalk = require('chalk')
const stealTools = require('steal-tools')
const { buildViewerHtml } = require('../demo/build.viewer.html')

const srcPath = path.join(__dirname, '..')
const outputPath = path.join(srcPath, 'dist_local')

console.log(chalk.cyan('🚀 Starting Optimized Local Build...'))

const buildConfig = {
main: 'app',
config: path.join(srcPath, 'package.json!npm')
}

const buildOptions = {
minify: true,
bundleSteal: true,
dest: path.join(outputPath, 'dist')
}

// 1. Clean up previous build results
fs.removeSync(outputPath)

// 2. Execute StealJS Build
stealTools.build(buildConfig, buildOptions)
.then(function () {
console.log(chalk.yellow('📦 JS Build finished. Syncing static assets...'))
makePackageFolder()
console.log(chalk.green('✨ Build Complete! Assets are located in: ' + outputPath))
console.log(chalk.blue('🔗 Command: npx http-server dist_local'))
})
.catch(function (error) {
console.log(chalk.red('❌ Build failed!'))
throw error
})

/**
* Orchestrates the copying of all necessary static files into the distribution folder.
*/
function makePackageFolder () {
// Ensure the output directory exists
fs.ensureDirSync(outputPath)

// Generate the shell HTML (local_viewer.html) using the imported utility
buildViewerHtml(true)

// Copy interview-specific guides and default content
fs.copySync(path.join(srcPath, 'demo/guides/default/'), path.join(outputPath, 'guides', 'default'))

// Copy viewer-related templates and static assets
fs.copySync(path.join(srcPath, 'scripts/viewer/'), outputPath)

// Copy global styles and generic images
fs.copySync(path.join(srcPath, 'styles', 'viewer-avatars.css'), path.join(outputPath, 'styles', 'viewer-avatars.css'))
fs.copySync(path.join(srcPath, 'images'), path.join(outputPath, 'images'))

// Ensure the default guide folder exists in the source for local development
fs.ensureDirSync(path.join(srcPath, 'scripts/guides/default/'))
fs.copySync(path.join(srcPath, 'scripts/guides/default/'), path.join(outputPath, 'guides', 'default'))

// Sync dependency assets: Avatar images from @caliorg
const depsPath = path.join(srcPath, 'node_modules', '@caliorg', 'a2jdeps', 'avatar', 'images')
if (fs.existsSync(depsPath)) {
fs.copySync(depsPath, path.join(outputPath, 'node_modules', '@caliorg', 'a2jdeps', 'avatar', 'images'))
}

// Sync dependency assets: Lightbox2 UI elements
const lbPath = path.join(srcPath, 'node_modules', 'lightbox2', 'dist', 'images')
if (fs.existsSync(lbPath)) {
fs.copySync(lbPath, path.join(outputPath, 'node_modules', 'lightbox2', 'dist', 'images'))
}

console.log(chalk.yellow('✅ All static assets successfully merged into dist_local.'))
}
30 changes: 30 additions & 0 deletions src/modal/modal-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AppState from '~/src/models/app-state'
import Interview from '~/src/models/interview'
import Logic from '~/src/mobile/util/logic'
import MemoryState from '~/src/models/memory-state'
import ModalContent from '~/src/models/modal-content'
import F from 'funcunit'
import { assert } from 'chai'
import sinon from 'sinon'
Expand Down Expand Up @@ -180,5 +181,34 @@ describe('<a2j-modal> ', function () {

assert.isTrue(pauseActivePlayersSpy.calledOnce, 'should fire pauseActivePlayers() on modal close to pause audio and video players')
})

it('historyLength returns the number of saved modal history entries', function () {
vm.modalHistory.push({ title: 'First' }, { title: 'Second' })
assert.equal(vm.historyLength, 2)
})

it('goBack() restores the previous modal content and removes it from history', function () {
vm.modalContent = new ModalContent({ title: 'Current', text: 'Current text' })
const assignSpy = sinon.spy(vm.modalContent, 'assign')
vm.modalHistory.push({ title: 'Previous', text: 'Previous text' })

vm.goBack()

assert.isTrue(assignSpy.calledOnce, 'should assign previous content when history exists')
assert.equal(vm.modalContent.title, 'Previous')
assert.equal(vm.modalContent.text, 'Previous text')
assert.equal(vm.historyLength, 0)
})

it('goBack() does nothing when modalHistory is empty', function () {
vm.modalContent = new ModalContent({ title: 'Current', text: 'Current text' })
const assignSpy = sinon.spy(vm.modalContent, 'assign')

vm.goBack()

assert.isFalse(assignSpy.called, 'should not assign anything when history is empty')
assert.equal(vm.modalContent.title, 'Current')
assert.equal(vm.historyLength, 0)
})
})
})
31 changes: 31 additions & 0 deletions src/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ export let ModalVM = DefineMap.extend('ViewerModalVM', {
interview: {},
appState: {},

modalHistory: {
default: () => []
},

get historyLength () {
return this.modalHistory.length
},

goBack () {
if (this.modalHistory.length > 0) {
// get latest modal content from history stack and set as current modal content
const previousContent = this.modalHistory.pop()
this.modalContent.assign(previousContent)
}
},

// set in appState and cleared here on close
modalContent: {
Type: ModalContent
Expand Down Expand Up @@ -97,6 +113,8 @@ export let ModalVM = DefineMap.extend('ViewerModalVM', {
this.triggeringElement.focus()
}

// clear history when closing modal to prevent going back to stale content
this.modalHistory = []
// clear for next modal use
this.modalContent = null
},
Expand Down Expand Up @@ -200,6 +218,19 @@ export default Component.extend({
analytics.trackCustomEvent('Pop-Up', 'from: ' + sourcePageName, pageName)
}

// push to the history
vm.modalHistory.push({
title: vm.modalContent.title,
text: vm.modalContent.text,
textAudioURL: vm.modalContent.textAudioURL,
imageURL: vm.modalContent.imageURL,
altText: vm.modalContent.altText,
mediaLabel: vm.modalContent.mediaLabel,
audioURL: vm.modalContent.audioURL,
videoURL: vm.modalContent.videoURL,
helpReader: vm.modalContent.helpReader
})

// popups now have text, audio, video and their alt-text values
// but title is internal descriptor so set to empty string
vm.modalContent.assign({
Expand Down
20 changes: 20 additions & 0 deletions src/modal/modal.less
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ a2j-modal {
}
}
}
.modal-title {
.back {
text-shadow: none;
color: #fff;
background: 0 0;
border: 0;
padding: 0;
opacity: 0.7;
&:focus, &:hover, &:active {
opacity: 1;
outline-color: #fff;

@supports (-webkit-hyphens:none) { /* safari only */
/* Safari doesn't allow you to change the focus outline color, we have to do this instead for a11y. */
outline: none;
box-shadow: 0 0 2px #fff, 0 0 2px #fff, 0 0 2px #fff, 0 0 2px #fff;
}
}
}
}
}

a.zoom-button {
Expand Down
14 changes: 12 additions & 2 deletions src/modal/modal.stache
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,20 @@
</button>

<h4 class="modal-title" id="pageModalLbl">
{{#if(modalHistory.length > 1)}}
<button
type="button"
class="back"
on:click="goBack()"
title="Go back to previous content"
aria-label="Go back to previous content"
tabindex="1"
>
<span class="glyphicon-left-open" aria-hidden="true"></span>
</button>
{{/if}}
{{#if(modalContent.title)}}
{{ parseText(cleanHTML(modalContent.title)) }}
{{else}}
<span class="glyphicon-lifebuoy" aria-hidden="true"></span>
{{/if}}
</h4>
</div>
Expand Down