summaryrefslogtreecommitdiff
path: root/frontend/app/views
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/views')
-rw-r--r--frontend/app/views/audio/components/audio.select.js58
-rw-r--r--frontend/app/views/graph/components/audio.list.js148
-rw-r--r--frontend/app/views/graph/components/graph.header.js4
-rw-r--r--frontend/app/views/graph/components/page.edit.js1
-rw-r--r--frontend/app/views/graph/components/page.form.js63
-rw-r--r--frontend/app/views/graph/graph.actions.js15
-rw-r--r--frontend/app/views/graph/graph.container.js2
-rw-r--r--frontend/app/views/graph/graph.css111
-rw-r--r--frontend/app/views/graph/graph.reducer.js45
-rw-r--r--frontend/app/views/index/containers/graph.index.js1
-rw-r--r--frontend/app/views/page/components/page.editor.js32
-rw-r--r--frontend/app/views/page/components/page.header.js20
-rw-r--r--frontend/app/views/page/components/tile.handle.js146
-rw-r--r--frontend/app/views/page/cursors.css7
-rw-r--r--frontend/app/views/page/page.actions.js24
-rw-r--r--frontend/app/views/page/page.container.js13
-rw-r--r--frontend/app/views/page/page.css9
-rw-r--r--frontend/app/views/page/page.reducer.js29
-rw-r--r--frontend/app/views/tile/components/tile.edit.js (renamed from frontend/app/views/page/components/tile.edit.js)4
-rw-r--r--frontend/app/views/tile/components/tile.form.js (renamed from frontend/app/views/page/components/tile.form.js)388
-rw-r--r--frontend/app/views/tile/components/tile.handle.js8
-rw-r--r--frontend/app/views/tile/components/tile.list.js (renamed from frontend/app/views/page/components/tile.list.js)12
-rw-r--r--frontend/app/views/tile/components/tile.new.js (renamed from frontend/app/views/page/components/tile.new.js)1
-rw-r--r--frontend/app/views/tile/handles/index.js14
-rw-r--r--frontend/app/views/tile/handles/tile.image.js56
-rw-r--r--frontend/app/views/tile/handles/tile.link.js33
-rw-r--r--frontend/app/views/tile/handles/tile.script.js36
-rw-r--r--frontend/app/views/tile/handles/tile.text.js44
-rw-r--r--frontend/app/views/tile/handles/tile.video.js108
-rw-r--r--frontend/app/views/tile/tile.utils.js80
30 files changed, 1298 insertions, 214 deletions
diff --git a/frontend/app/views/audio/components/audio.select.js b/frontend/app/views/audio/components/audio.select.js
new file mode 100644
index 0000000..384bb7a
--- /dev/null
+++ b/frontend/app/views/audio/components/audio.select.js
@@ -0,0 +1,58 @@
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+
+import { Select } from 'app/common'
+import { unslugify } from 'app/utils'
+
+const NO_AUDIO = 0
+const AUDIO_TOP_OPTIONS = [
+ { name: NO_AUDIO, label: 'No Sound' },
+ { name: -2, label: '──────────', disabled: true },
+]
+
+class AudioSelect extends Component {
+ state = {
+ audioList: []
+ }
+
+ constructor(props) {
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ }
+
+ componentDidMount(){
+ const { uploads } = this.props.graph.show.res
+ const audioUploads = uploads
+ .filter(upload => upload.tag === 'audio')
+ .map(upload => ({ name: upload.id, label: unslugify(upload.fn) }))
+ let audioList = [
+ ...AUDIO_TOP_OPTIONS,
+ ...audioUploads,
+ ]
+ this.setState({
+ audioList,
+ })
+ }
+
+ handleChange(name, value) {
+ this.props.onChange(name, parseInt(value))
+ }
+
+ render() {
+ return (
+ <Select
+ title={this.props.title || "Audio"}
+ name={this.props.name}
+ selected={this.props.selected || NO_AUDIO}
+ options={this.state.audioList}
+ onChange={this.handleChange}
+ />
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ graph: state.graph,
+})
+
+export default connect(mapStateToProps)(AudioSelect)
diff --git a/frontend/app/views/graph/components/audio.list.js b/frontend/app/views/graph/components/audio.list.js
new file mode 100644
index 0000000..011ab08
--- /dev/null
+++ b/frontend/app/views/graph/components/audio.list.js
@@ -0,0 +1,148 @@
+import React, { Component } from 'react'
+import { Link } from 'react-router-dom'
+import { connect } from 'react-redux'
+
+import { history } from 'app/store'
+import { unslugify } from 'app/utils'
+import actions from 'app/actions'
+
+class AudioList extends Component {
+ state = {
+ playing: false,
+ play_id: -1,
+ }
+
+ constructor(props) {
+ super(props)
+ this.toggleAudio = this.toggleAudio.bind(this)
+ this.upload = this.upload.bind(this)
+ this.audioDidEnd = this.audioDidEnd.bind(this)
+ }
+
+ componentDidMount() {
+ this.audioElement = document.createElement('audio')
+ this.audioElement.addEventListener('ended', this.audioDidEnd)
+ }
+
+ componentWillUnmount() {
+ this.audioElement.removeEventListener('ended', this.audioDidEnd)
+ this.audioElement.pause()
+ this.audioElement = null
+ }
+
+ audioDidEnd() {
+ this.setState({ playing: false })
+ }
+
+ upload(e) {
+ e.preventDefault()
+ document.body.className = ''
+ const files = e.dataTransfer ? e.dataTransfer.files : e.target.files
+ let i
+ if (!files.length) return
+ Array.from(files).forEach(file => this.uploadTaggedFile(file, 'audio', file.filename))
+ }
+
+ uploadTaggedFile(file, tag, fn) {
+ return new Promise((resolve, reject) => {
+ this.setState({ status: "Uploading " + tag + "..." })
+ const uploadData = {
+ tag,
+ file,
+ __file_filename: fn,
+ graph_id: this.props.graph.id,
+ username: 'swimmer',
+ }
+ // console.log(uploadData)
+ return actions.upload.upload(uploadData).then(data => {
+ // console.log(data)
+ resolve({
+ ...data.res,
+ })
+ })
+ })
+ }
+
+ destroyFile(upload) {
+ return new Promise((resolve, reject) => {
+ actions.upload.destroy(upload)
+ .then(() => {
+ console.log('Destroy successful')
+ resolve()
+ })
+ .catch(() => {
+ console.log('Error deleting the file')
+ reject()
+ })
+ })
+ }
+
+ toggleAudio(upload) {
+ console.log(upload)
+ let playing = false
+ if (this.state.play_id === upload.id && this.state.playing) {
+ this.audioElement.pause()
+ } else {
+ this.audioElement.src = upload.url
+ this.audioElement.currentTime = 0
+ this.audioElement.play()
+ playing = true
+ }
+ this.setState({
+ playing,
+ play_id: upload.id,
+ })
+ }
+
+ render() {
+ const { playing, play_id } = this.state
+ const { graph } = this.props
+ // console.log(graph.uploads)
+ return (
+ <div className='box audioList'>
+ <div className="uploadButton">
+ <button>
+ <span>
+ {"Upload an audio file"}
+ </span>
+ </button>
+ <input
+ type="file"
+ accept="audio/mp3"
+ onChange={this.upload}
+ required={this.props.required}
+ />
+ </div>
+ {graph.uploads.map(upload => (
+ <div className='audioItem' key={upload.id} onClick={() => this.toggleAudio(upload)} >
+ <img
+ className='playButton'
+ src={
+ (playing && play_id === upload.id)
+ ? "/static/img/icons_pause_white.svg"
+ : "/static/img/icons_play_white.svg"
+ }
+ />
+ <div className='title'>
+ <div>{unslugify(upload.fn)}</div>
+ </div>
+ </div>
+ ))}
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ graph: state.graph.show.res,
+})
+
+const mapDispatchToProps = dispatch => ({
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(AudioList)
+
+
+/*
+ - upload new audio file
+ */ \ No newline at end of file
diff --git a/frontend/app/views/graph/components/graph.header.js b/frontend/app/views/graph/components/graph.header.js
index 46ad962..0766580 100644
--- a/frontend/app/views/graph/components/graph.header.js
+++ b/frontend/app/views/graph/components/graph.header.js
@@ -9,10 +9,12 @@ function GraphHeader(props) {
return (
<header>
<div>
- <Link to="/" className="logo"><b>{props.site.siteTitle}</b></Link>
+ <Link to="/" className="logo arrow">{"◁ "}</Link>
+ <b>{props.site.siteTitle}</b>
</div>
<div>
<button onClick={() => props.graphActions.toggleAddPageForm()}>+ Add page</button>
+ <button onClick={() => props.graphActions.toggleAudioList()}>+ Audio</button>
</div>
</header>
)
diff --git a/frontend/app/views/graph/components/page.edit.js b/frontend/app/views/graph/components/page.edit.js
index 4025726..16a7eef 100644
--- a/frontend/app/views/graph/components/page.edit.js
+++ b/frontend/app/views/graph/components/page.edit.js
@@ -45,6 +45,7 @@ class PageEdit extends Component {
return (
<PageForm
data={show.res}
+ actions={{ graph: this.props.graphActions }}
graph={this.props.graph.show.res}
onSubmit={this.handleSubmit.bind(this)}
/>
diff --git a/frontend/app/views/graph/components/page.form.js b/frontend/app/views/graph/components/page.form.js
index 8fc00b0..a060698 100644
--- a/frontend/app/views/graph/components/page.form.js
+++ b/frontend/app/views/graph/components/page.form.js
@@ -2,8 +2,11 @@ import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { session } from 'app/session'
+import actions from 'app/actions'
+import { history } from 'app/store'
-import { TextInput, ColorInput, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from 'app/common'
+import { TextInput, ColorInput, Checkbox, LabelDescription, TextArea, SubmitButton, Loader } from 'app/common'
+import AudioSelect from 'app/views/audio/components/audio.select'
const newPage = (data) => ({
path: '',
@@ -14,6 +17,8 @@ const newPage = (data) => ({
x: 0.05,
y: 0.05,
background_color: '#000000',
+ background_audio_id: 0,
+ restart_audio: false,
},
...data,
})
@@ -26,6 +31,16 @@ export default class PageForm extends Component {
errorFields: new Set([]),
}
+ constructor(props){
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ this.handleSelect = this.handleSelect.bind(this)
+ this.handleSettingsChange = this.handleSettingsChange.bind(this)
+ this.handleSettingsSelect = this.handleSettingsSelect.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ this.handleDelete = this.handleDelete.bind(this)
+ }
+
componentDidMount() {
const { graph, data, isNew } = this.props
const title = isNew ? 'new page' : 'editing ' + data.title
@@ -76,6 +91,10 @@ export default class PageForm extends Component {
handleSettingsChange(e) {
const { name, value } = e.target
+ this.handleSettingsSelect(name, value)
+ }
+
+ handleSettingsSelect(name, value) {
this.setState({
data: {
...this.state.data,
@@ -110,11 +129,17 @@ export default class PageForm extends Component {
}
}
- handleDelete() {
+ handleDelete(e) {
+ e && e.preventDefault()
+ e && e.stopPropagation()
const { data } = this.state
console.log(data)
if (confirm('Really delete this page?')) {
- actions.page.delete(page_id)
+ actions.page.destroy(data)
+ .then(() => {
+ this.props.actions.graph.hideEditPageForm()
+ history.goBack()
+ })
}
}
@@ -124,14 +149,14 @@ export default class PageForm extends Component {
return (
<div className='box'>
<h1>{title}</h1>
- <form onSubmit={this.handleSubmit.bind(this)}>
+ <form onSubmit={this.handleSubmit}>
<TextInput
title="Path"
name="path"
required
data={data}
error={errorFields.has('path')}
- onChange={this.handleChange.bind(this)}
+ onChange={this.handleChange}
autoComplete="off"
/>
<LabelDescription>
@@ -143,32 +168,48 @@ export default class PageForm extends Component {
required
data={data}
error={errorFields.has('title')}
- onChange={this.handleChange.bind(this)}
+ onChange={this.handleChange}
autoComplete="off"
/>
<ColorInput
- title='BG'
+ title='BG Color'
name='background_color'
data={data.settings}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<TextArea
title="Description"
name="description"
data={data}
- onChange={this.handleChange.bind(this)}
+ onChange={this.handleChange}
+ />
+
+ <AudioSelect
+ title="Background Audio"
+ name="background_audio_id"
+ selected={data.settings.background_audio_id}
+ onChange={this.handleSettingsSelect}
/>
+
+ <Checkbox
+ label="Restart audio on load"
+ name="restart_audio"
+ checked={data.settings.restart_audio}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+
<div className='row buttons'>
<SubmitButton
title={submitTitle}
- onClick={this.handleSubmit.bind(this)}
+ onClick={this.handleSubmit}
/>
{!isNew &&
<SubmitButton
title={'Delete'}
className='destroy'
- onClick={this.handleDelete.bind(this)}
+ onClick={this.handleDelete}
/>
}
</div>
diff --git a/frontend/app/views/graph/graph.actions.js b/frontend/app/views/graph/graph.actions.js
index a24ccc2..4185386 100644
--- a/frontend/app/views/graph/graph.actions.js
+++ b/frontend/app/views/graph/graph.actions.js
@@ -1,4 +1,5 @@
import * as types from 'app/types'
+import { api } from 'app/utils'
import actions from 'app/actions'
export const showAddPageForm = () => dispatch => {
@@ -25,6 +26,10 @@ export const toggleEditPageForm = () => dispatch => {
dispatch({ type: types.graph.toggle_edit_page_form })
}
+export const toggleAudioList = () => dispatch => {
+ dispatch({ type: types.graph.toggle_audio_list })
+}
+
export const updateGraphPage = page => dispatch => {
dispatch({ type: types.graph.update_graph_page, page })
}
@@ -34,4 +39,12 @@ export const setHomePageId = (graph, page) => dispatch => {
delete updated_graph.pages
updated_graph.home_page_id = page.id
actions.graph.update(updated_graph)
-} \ No newline at end of file
+}
+
+export const viewPage = (graph, page) => dispatch => {
+ api(dispatch, types.api, 'export', `/api/v1/graph/export/${graph.path}`)
+ .then(result => {
+ console.log(result)
+ window.open(`${process.env.EXPORT_HOST}/${graph.path}/${page.path}`)
+ })
+}
diff --git a/frontend/app/views/graph/graph.container.js b/frontend/app/views/graph/graph.container.js
index 9e354fc..34c3d9d 100644
--- a/frontend/app/views/graph/graph.container.js
+++ b/frontend/app/views/graph/graph.container.js
@@ -15,6 +15,7 @@ import PageEdit from './components/page.edit'
import GraphHeader from './components/graph.header'
import GraphEditor from './components/graph.editor'
+import AudioList from './components/audio.list'
class GraphContainer extends Component {
componentDidMount() {
@@ -63,6 +64,7 @@ class GraphContainer extends Component {
<div className='sidebar'>
{this.props.graph.editor.addingPage && <PageNew />}
{this.props.graph.editor.editingPage && <PageEdit />}
+ {this.props.graph.editor.showingAudio && <AudioList />}
</div>
</div>
</div>
diff --git a/frontend/app/views/graph/graph.css b/frontend/app/views/graph/graph.css
index 389a55d..171bb38 100644
--- a/frontend/app/views/graph/graph.css
+++ b/frontend/app/views/graph/graph.css
@@ -29,6 +29,10 @@
max-height: 100%;
z-index: 20;
}
+.sidebar.left {
+ right: auto;
+ left: 0;
+}
.box {
width: 15rem;
padding: 0.5rem;
@@ -65,6 +69,9 @@
justify-content: flex-start;
align-items: center;
}
+.box form label.checkbox.short span {
+ padding: 0.125rem 0;
+}
.box form input[type="checkbox"] {
margin-left: 0rem;
}
@@ -100,6 +107,9 @@
padding: 0.25rem;
margin-right: 0;
}
+.box .single .select {
+ width: 9.25rem;
+}
.box .selects label {
flex-direction: row;
width: 6.5rem;
@@ -110,6 +120,18 @@
padding: 0.25rem;
}
+.box form .single label span {
+ min-width: auto;
+ width: 4.25rem;
+ padding: 0.25rem 0;
+}
+.box .single label {
+ flex-direction: row;
+ width: 100%;
+ margin-right: 0.5px;
+ min-width: auto;
+}
+
.box form .pair label span {
min-width: 3rem;
padding: 0.25rem 0;
@@ -123,11 +145,45 @@
.box .pair input[type=text] {
width: 3rem;
}
+
+.box form .single_text label span {
+ min-width: auto;
+ width: 6rem;
+ padding: 0.25rem 0;
+}
+.box .single_text input[type=text] {
+ width: 7rem;
+}
+.box .single_text label {
+ flex-direction: row;
+ justify-content: space-between;
+ width: 100%;
+ margin-right: 0.5px;
+ min-width: auto;
+}
+
.box .position {
font-size: smaller;
margin-bottom: 0.25rem;
}
+button.box_corner {
+ position: absolute;
+ top: 1.25rem; right: 1.25rem;
+ padding: 0.5rem;
+ background: transparent;
+ border: 0;
+ border-radius: 4px;
+ transform: scaleX(-1);
+}
+button.box_corner:hover {
+ color: #fff;
+ background: rgba(64,64,128,0.5);
+}
+.sidebar.left button.box_corner {
+ transform: scaleX(1);
+}
+
.box .slider {
display: flex;
flex-direction: row;
@@ -146,6 +202,59 @@
width: 5.5rem;
}
+/* Upload area */
+
+.box .uploadButton {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+.uploadButton input[type=file] {
+ position: absolute;
+ top: 0; left: 0;
+ width: 100%; height: 100%;
+}
+.audioList .audioItem {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ cursor: pointer;
+ padding: 0.125rem 0;
+}
+.audioList .playButton {
+ background: transparent;
+ border: 0;
+ width: 1.5rem;
+ height: 1.5rem;
+ margin-right: 0.5rem;
+ opacity: 0.8;
+}
+.audioList .title {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ overflow: hidden;
+ flex: 1;
+}
+.audioList .title div {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre;
+ width: 100%;
+}
+.audioList .audioItem:hover {
+ background: rgba(255,255,255,0.2);
+}
+.audioList .audioItem:hover .title {
+ color: #fff;
+}
+.audioList .audioItem:hover .playButton {
+ opacity: 1.0;
+}
+
/* Graph handles */
.handle {
@@ -156,7 +265,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- max-width: 8rem;
+ max-width: 12rem;
user-select: none;
cursor: arrow;
}
diff --git a/frontend/app/views/graph/graph.reducer.js b/frontend/app/views/graph/graph.reducer.js
index 6be5089..725c256 100644
--- a/frontend/app/views/graph/graph.reducer.js
+++ b/frontend/app/views/graph/graph.reducer.js
@@ -7,6 +7,8 @@ const initialState = crudState('graph', {
editor: {
addingPage: false,
editingPage: false,
+ showingAudio: false,
+ building: false,
},
options: {
}
@@ -36,6 +38,19 @@ export default function graphReducer(state = initialState, action) {
}
}
+ case types.upload.upload_complete:
+ console.log(action)
+ return {
+ ...state,
+ show: {
+ ...state.show,
+ res: {
+ ...state.show.res,
+ uploads: state.show.res.uploads.concat(action.data.res)
+ }
+ }
+ }
+
case types.graph.show_add_page_form:
return {
...state,
@@ -43,6 +58,7 @@ export default function graphReducer(state = initialState, action) {
...state.editor,
addingPage: true,
editingPage: false,
+ showingAudio: false,
}
}
@@ -52,6 +68,7 @@ export default function graphReducer(state = initialState, action) {
editor: {
...state.editor,
addingPage: false,
+ showingAudio: false,
}
}
@@ -62,6 +79,7 @@ export default function graphReducer(state = initialState, action) {
...state.editor,
addingPage: !state.editor.addingPage,
editingPage: false,
+ showingAudio: false,
}
}
@@ -72,6 +90,7 @@ export default function graphReducer(state = initialState, action) {
...state.editor,
addingPage: false,
editingPage: true,
+ showingAudio: false,
}
}
@@ -81,6 +100,7 @@ export default function graphReducer(state = initialState, action) {
editor: {
...state.editor,
editingPage: false,
+ showingAudio: false,
}
}
@@ -91,9 +111,34 @@ export default function graphReducer(state = initialState, action) {
...state.editor,
addingPage: false,
editingPage: !state.editor.editingPage,
+ showingAudio: false,
}
}
+ case types.graph.toggle_audio_list:
+ return {
+ ...state,
+ editor: {
+ ...state.editor,
+ addingPage: false,
+ editingPage: false,
+ showingAudio: !state.editor.showingAudio,
+ }
+ }
+
+ case types.api.loading:
+ if (action.tag !== 'view' && action.tag !== 'export') {
+ return state
+ }
+ return { ...state, editor: { ...state.editor, building: action.tag } }
+
+ case types.api.loaded:
+ case types.api.error:
+ if (action.tag !== 'view' && action.tag !== 'export') {
+ return state
+ }
+ return { ...state, editor: { ...state.editor, building: null } }
+
default:
return state
}
diff --git a/frontend/app/views/index/containers/graph.index.js b/frontend/app/views/index/containers/graph.index.js
index 91098a7..bf3d75e 100644
--- a/frontend/app/views/index/containers/graph.index.js
+++ b/frontend/app/views/index/containers/graph.index.js
@@ -11,6 +11,7 @@ class GraphIndex extends Component {
componentDidMount() {
actions.graph.index()
}
+
render() {
const { index } = this.props
// console.log(this.props)
diff --git a/frontend/app/views/page/components/page.editor.js b/frontend/app/views/page/components/page.editor.js
index d324874..ec6ddd3 100644
--- a/frontend/app/views/page/components/page.editor.js
+++ b/frontend/app/views/page/components/page.editor.js
@@ -5,17 +5,18 @@ import { connect } from 'react-redux'
import { session } from 'app/session'
import actions from 'app/actions'
-import * as pageActions from '../page.actions'
-import * as tileActions from '../../tile/tile.actions'
+import * as pageActions from 'app/views/page/page.actions'
+import * as tileActions from 'app/views/tile/tile.actions'
import { Loader } from 'app/common'
import { clamp, dist } from 'app/utils'
-import TileHandle from './tile.handle'
+import TileHandle from 'app/views/tile/components/tile.handle'
const defaultState = {
dragging: false,
bounds: null,
+ videoBounds: null,
mouseX: 0,
mouseY: 0,
box: {
@@ -37,6 +38,7 @@ class PageEditor extends Component {
this.handleMouseMove = this.handleMouseMove.bind(this)
this.handleMouseUp = this.handleMouseUp.bind(this)
this.handleWindowResize = this.handleWindowResize.bind(this)
+ this.handlePlaybackEnded = this.handlePlaybackEnded.bind(this)
this.pageRef = React.createRef()
}
@@ -59,7 +61,8 @@ class PageEditor extends Component {
document.body.addEventListener('mousemove', this.handleMouseMove)
document.body.addEventListener('mouseup', this.handleMouseUp)
window.addEventListener('resize', this.handleWindowResize)
- this.setState({ bounds: this.getBoundingClientRect() })
+ const bounds = this.getBoundingClientRect()
+ this.setState({ bounds })
}
componentDidUpdate(prevProps) {
@@ -72,7 +75,12 @@ class PageEditor extends Component {
this.setState({ bounds: this.getBoundingClientRect() })
}
+ handlePlaybackEnded() {
+ //
+ }
+
handleMouseDown(e, tile) {
+ if (e.metaKey || e.ctrlKey || e.altKey || e.button !== 0) return
const bounds = this.getBoundingClientRect()
const mouseX = e.pageX
const mouseY = e.pageY
@@ -130,7 +138,7 @@ class PageEditor extends Component {
const { dx, dy } = box
let url = window.location.pathname
this.setState({
- page: null,
+ tile: null,
box: null,
initialBox: null,
dragging: false,
@@ -142,6 +150,7 @@ class PageEditor extends Component {
}
const updatedTile = {
...tile,
+ target_page_id: tile.target_page_id || 0,
settings: {
...tile.settings,
x: tile.settings.x + dx,
@@ -158,7 +167,7 @@ class PageEditor extends Component {
}
}
- render(){
+ render() {
if (!this.state.bounds || (!this.props.page.show.res && !this.props.page.show.res.tiles)) {
return (
<div className='page' ref={this.pageRef} />
@@ -170,9 +179,14 @@ class PageEditor extends Component {
const { res } = this.props.page.show
const { settings } = res
const pageStyle = { backgroundColor: settings ? settings.background_color : '#000000' }
+ const videoBounds = (res.tiles && res.tiles.length && res.tiles[0].type === 'video') ? {
+ width: res.tiles[0].settings.width,
+ height: res.tiles[0].settings.height,
+ } : this.state.bounds
return (
<div className='page' ref={this.pageRef} style={pageStyle}>
- {res.tiles.map(tile => {
+ {res.tiles && res.tiles.map(tile => {
+ if (!this.props.page.editor.showingPopups && tile.settings.is_popup) return
if (temporaryTile && temporaryTile.id === tile.id) {
tile = temporaryTile
}
@@ -181,9 +195,11 @@ class PageEditor extends Component {
key={tile.id}
tile={tile}
bounds={this.state.bounds}
+ videoBounds={videoBounds}
box={currentTile && tile.id === currentTile.id && currentBox}
onMouseDown={e => this.handleMouseDown(e, tile)}
onDoubleClick={e => this.props.pageActions.showEditTileForm(tile.id)}
+ onPlaybackEnded={this.handlePlaybackEnded}
/>
)
})}
@@ -192,8 +208,10 @@ class PageEditor extends Component {
key={temporaryTile.id}
tile={temporaryTile}
bounds={this.state.bounds}
+ videoBounds={videoBounds}
box={currentTile && temporaryTile.id === currentTile.id && currentBox}
onMouseDown={e => this.handleMouseDown(e, temporaryTile)}
+ onPlaybackEnded={this.handlePlaybackEnded}
/>
)}
</div>
diff --git a/frontend/app/views/page/components/page.header.js b/frontend/app/views/page/components/page.header.js
index eb1c3b9..d40f6e0 100644
--- a/frontend/app/views/page/components/page.header.js
+++ b/frontend/app/views/page/components/page.header.js
@@ -3,6 +3,9 @@ import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
+import { Loader } from 'app/common'
+import { capitalize } from 'app/utils'
+
import * as graphActions from '../../graph/graph.actions'
import * as pageActions from '../page.actions'
@@ -10,12 +13,23 @@ function PageHeader(props) {
return (
<header>
<div>
- <Link to={props.graph.show.res ? "/" + props.graph.show.res.path : "/"} className="logo"><b>{props.site.siteTitle}</b></Link>
+ <Link to={props.graph ? "/" + props.graph.path : "/"} className="logo arrow">{"◁"}</Link>
+ <b>{props.site.siteTitle}</b>
+ {props.building && (
+ <div className='building'>
+ <div className='loader'>
+ <Loader />
+ </div>
+ {capitalize(props.building)}ing...
+ </div>
+ )}
</div>
<div>
<button onClick={() => props.pageActions.toggleAddTileForm()}>+ Add tile</button>
<button onClick={() => props.pageActions.toggleTileList()}>Sort tiles</button>
+ <button onClick={() => props.pageActions.togglePopups()}>Toggle popups</button>
<button onClick={() => props.graphActions.toggleEditPageForm()}>Edit page</button>
+ <button onClick={() => props.graphActions.viewPage(props.graph, props.page)}>View page</button>
</div>
</header>
)
@@ -24,7 +38,9 @@ function PageHeader(props) {
const mapStateToProps = (state) => ({
// auth: state.auth,
site: state.site,
- graph: state.graph,
+ graph: state.graph.show.res,
+ page: state.page.show.res,
+ building: state.graph.editor.building,
// isAuthenticated: state.auth.isAuthenticated,
})
diff --git a/frontend/app/views/page/components/tile.handle.js b/frontend/app/views/page/components/tile.handle.js
deleted file mode 100644
index 624b175..0000000
--- a/frontend/app/views/page/components/tile.handle.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import React, { Component } from 'react'
-import { Link } from 'react-router-dom'
-
-const TileHandle = ({ tile, bounds, box, viewing, onMouseDown, onDoubleClick }) => {
- // console.log(tile)
- const { width, height } = tile.settings
- const style = {
- transform: generateTransform(tile, box),
- opacity: tile.settings.opacity,
- }
- // console.log(generateTransform(tile))
- let content;
- let className = ['tile', tile.type].join(' ')
- if (tile.target_page_id || (viewing && tile.href)) {
- className += ' ' + (tile.settings.cursor || 'hand_up')
- }
- // console.log(tile.settings)
- switch (tile.type) {
- case 'image':
- if (!tile.settings.url) {
- return null
- }
- if (tile.settings.is_tiled) {
- style.backgroundImage = 'url(' + tile.settings.url + ')'
- style.backgroundPosition = tile.settings.align.replace('_', ' ')
- switch (tile.settings.tile_style) {
- default:
- case 'tile':
- break
- case 'cover':
- style.backgroundSize = 'cover'
- break
- case 'contain':
- style.backgroundSize = 'contain'
- break
- case 'contain no-repeat':
- style.backgroundSize = 'contain'
- style.backgroundRepeat = 'no-repeat'
- break
- }
- className += ' is_tiled'
- } else {
- className += ' ' + tile.settings.align
- content = <img src={tile.settings.url} />
- }
- break
- case 'text':
- if (!tile.settings.content) {
- return null
- }
- content = <span dangerouslySetInnerHTML={{ __html: tile.settings.content }} />
- className += ' ' + tile.settings.align
- style.width = tile.settings.width ? tile.settings.width + 'px' : 'auto'
- style.height = tile.settings.height ? tile.settings.height + 'px' : 'auto'
- style.fontFamily = tile.settings.font_family
- style.fontSize = tile.settings.font_size + 'px'
- style.lineHeight = 1.5
- style.fontWeight = (tile.settings.font_style || "").indexOf('bold') !== -1 ? 'bold' : 'normal'
- style.fontStyle = (tile.settings.font_style || "").indexOf('italic') !== -1 ? 'italic' : 'normal'
- style.backgroundColor = tile.settings.background_color || 'transparent'
- style.color = tile.settings.font_color || '#dddddd!important'
- break
- case 'link':
- content = ""
- className += ' ' + tile.settings.align
- style.width = tile.settings.width ? tile.settings.width + 'px' : 'auto'
- style.height = tile.settings.height ? tile.settings.height + 'px' : 'auto'
- break
- case 'script':
- content = ""
- if (viewing) {
- eval(tile.settings.content)
- } else {
- content = "SCRIPT"
- }
- }
- if (viewing && tile.href) {
- if (tile.href.indexOf('http') === 0) {
- return (
- <a href={tile.href} rel='noopener'>
- <div
- className={className}
- style={style}
- >
- {content}
- </div>
- </a>
- )
- } else {
- return (
- <Link to={tile.href}>
- <div
- className={className}
- onMouseDown={onMouseDown}
- style={style}
- >
- {content}
- </div>
- </Link>
- )
- }
- } else {
- return (
- <div
- className={className}
- onMouseDown={onMouseDown}
- onDoubleClick={onDoubleClick}
- style={style}
- >
- {content}
- </div>
- )
- }
-}
-
-const generateTransform = (tile, box) => {
- let { x, y, align, rotation, scale, is_tiled } = tile.settings
- if (is_tiled) {
- return 'translateZ(0)'
- }
- if (box) {
- x += box.dx
- y += box.dy
- }
- const [yalign, xalign] = align.split('_')
- let transform = ['translateZ(0)']
- if (yalign === 'center') {
- transform.push('translateY(-50%)')
- }
- if (xalign === 'center') {
- transform.push('translateX(-50%)')
- }
- // if (x % 2 == 1) x += 0.5
- // if (y % 2 == 1) y += 0.5
- transform.push('translateX(' + x + 'px)')
- transform.push('translateY(' + y + 'px)')
- if (scale !== 1) {
- transform.push('scale(' + scale + ')')
- }
- if (rotation !== 0) {
- transform.push('rotateZ(' + rotation + 'deg)')
- }
- return transform.join(' ')
-}
-
-export default TileHandle
diff --git a/frontend/app/views/page/cursors.css b/frontend/app/views/page/cursors.css
index 56fb088..6cc37a9 100644
--- a/frontend/app/views/page/cursors.css
+++ b/frontend/app/views/page/cursors.css
@@ -13,6 +13,13 @@
.tile.hand_left {
cursor: url(/static/img/hand_left.png) 10 60, pointer;
}
+.tile.unclickable {
+ cursor: default;
+ pointer-events: none;
+}
+.tile.none {
+ cursor: default;
+}
.tile.link {
cursor: pointer;
diff --git a/frontend/app/views/page/page.actions.js b/frontend/app/views/page/page.actions.js
index d2bbbe2..e42d539 100644
--- a/frontend/app/views/page/page.actions.js
+++ b/frontend/app/views/page/page.actions.js
@@ -50,6 +50,30 @@ export const toggleTileList = () => dispatch => {
dispatch({ type: types.page.toggle_tile_list })
}
+// Popups
+
+export const loadPopups = (page, popups) => dispatch => {
+ const state = store.getState()
+ page = page || state.page.show.res
+ popups = popups || state.page.editor.popups
+ popups = page.tiles.reduce((acc, tile) => {
+ const { is_popup, popup_group } = tile.settings
+ if (is_popup) {
+ acc[popup_group] = acc[popup_group] || false
+ }
+ return acc
+ }, { ...popups })
+ // console.log(popups)
+ dispatch({ type: types.page.load_popups, popups })
+}
+export const togglePopups = () => dispatch => {
+ dispatch({ type: types.page.toggle_popups })
+}
+
+export const toggleSidebarSide = () => dispatch => {
+ dispatch({ type: types.page.toggle_sidebar_side })
+}
+
// Update local page tile state when we change it
export const updatePageTile = tile => dispatch => {
diff --git a/frontend/app/views/page/page.container.js b/frontend/app/views/page/page.container.js
index dc85f5e..0ad9806 100644
--- a/frontend/app/views/page/page.container.js
+++ b/frontend/app/views/page/page.container.js
@@ -9,13 +9,13 @@ import './cursors.css'
import actions from 'app/actions'
import { Loader } from 'app/common'
-import * as graphActions from '../graph/graph.actions'
+import * as graphActions from 'app/views/graph/graph.actions'
import * as pageActions from './page.actions'
-import PageEdit from '../graph/components/page.edit'
-import TileNew from './components/tile.new'
-import TileEdit from './components/tile.edit'
-import TileList from './components/tile.list'
+import PageEdit from 'app/views/graph/components/page.edit'
+import TileNew from 'app/views/tile/components/tile.new'
+import TileEdit from 'app/views/tile/components/tile.edit'
+import TileList from 'app/views/tile/components/tile.list'
import PageHeader from './components/page.header'
import PageEditor from './components/page.editor'
@@ -44,6 +44,7 @@ class PageContainer extends Component {
this.props.pageActions.showGraphAndPageIfUnloaded(this.props.match.params)
.then(data => {
actions.site.setSiteTitle(data.res.title)
+ this.props.pageActions.loadPopups(data.res, {})
if (!data.res.tiles.length) {
this.props.pageActions.showAddTileForm()
} else {
@@ -70,7 +71,7 @@ class PageContainer extends Component {
<PageHeader />
<div className='body'>
<PageEditor />
- <div className='sidebar'>
+ <div className={this.props.page.editor.sidebarOnRight ? 'sidebar' : 'sidebar left'}>
{this.props.graph.editor.editingPage && <PageEdit />}
{this.props.page.editor.addingTile && <TileNew />}
{this.props.page.editor.editingTile && <TileEdit />}
diff --git a/frontend/app/views/page/page.css b/frontend/app/views/page/page.css
index 4559543..2e0efb9 100644
--- a/frontend/app/views/page/page.css
+++ b/frontend/app/views/page/page.css
@@ -15,6 +15,9 @@
.tile.image {
display: block;
}
+.tile.video {
+ display: block;
+}
.tile.image.is_tiled {
width: 100%;
height: 100%;
@@ -114,6 +117,12 @@
.box .row.pair {
justify-content: space-between;
}
+.box .row.pair.with_checkbox {
+ align-items: flex-end;
+}
+.box .row.single {
+ justify-content: space-between;
+}
.box .pair label:last-child {
margin-right: 0;
}
diff --git a/frontend/app/views/page/page.reducer.js b/frontend/app/views/page/page.reducer.js
index c2d231a..a1f281a 100644
--- a/frontend/app/views/page/page.reducer.js
+++ b/frontend/app/views/page/page.reducer.js
@@ -9,6 +9,9 @@ const initialState = crudState('page', {
editingTile: false,
currentEditTileId: 0,
tileList: false,
+ showingPopups: true,
+ sidebarOnRight: true,
+ popups: {},
},
options: {
}
@@ -195,6 +198,32 @@ export default function pageReducer(state = initialState, action) {
}
}
+ case types.page.toggle_popups:
+ return {
+ ...state,
+ editor: {
+ ...state.editor,
+ showingPopups: !state.editor.showingPopups,
+ }
+ }
+
+ case types.page.load_popups:
+ return {
+ ...state,
+ editor: {
+ ...state.editor,
+ popups: action.popups,
+ }
+ }
+
+ case types.page.toggle_sidebar_side:
+ return {
+ ...state,
+ editor: {
+ ...state.editor,
+ sidebarOnRight: !state.editor.sidebarOnRight,
+ }
+ }
default:
return state
diff --git a/frontend/app/views/page/components/tile.edit.js b/frontend/app/views/tile/components/tile.edit.js
index 2ea09d1..cae9f73 100644
--- a/frontend/app/views/page/components/tile.edit.js
+++ b/frontend/app/views/tile/components/tile.edit.js
@@ -29,7 +29,9 @@ class TileEdit extends Component {
load() {
const { currentEditTileId } = this.props.page.editor
- const tile = this.props.page.show.res.tiles.filter(tile => tile.id === currentEditTileId)[0]
+ const { tiles } = this.props.page.show.res
+ if (!tiles) return
+ const tile = tiles.filter(tile => tile.id === currentEditTileId)[0]
console.log('edit', currentEditTileId)
this.setState({ tile })
}
diff --git a/frontend/app/views/page/components/tile.form.js b/frontend/app/views/tile/components/tile.form.js
index 3f43dd0..8a6a08e 100644
--- a/frontend/app/views/page/components/tile.form.js
+++ b/frontend/app/views/tile/components/tile.form.js
@@ -10,12 +10,14 @@ import {
TextInput, NumberInput, ColorInput, Slider,
Select, LabelDescription, TextArea, Checkbox,
SubmitButton, Loader } from 'app/common'
-import { preloadImage } from 'app/utils'
+import AudioSelect from 'app/views/audio/components/audio.select'
+import { preloadImage, preloadVideo } from 'app/utils'
-import * as tileActions from '../../tile/tile.actions'
+import * as pageActions from 'app/views/page/page.actions'
+import * as tileActions from 'app/views/tile/tile.actions'
const SELECT_TYPES = [
- "image", "text", "link", "script",
+ "image", "text", "video", "link", "script",
].map(s => ({ name: s, label: s }))
const ALIGNMENTS = [
@@ -31,6 +33,7 @@ const ALIGNMENTS = [
const REQUIRED_KEYS = {
image: ['url'],
+ video: ['url'],
text: ['content'],
link: [],
script: [],
@@ -40,6 +43,10 @@ const IMAGE_TILE_STYLES = [
'tile', 'cover', 'contain', 'contain no-repeat'
].map(style => ({ name: style, label: style }))
+const VIDEO_STYLES = [
+ 'normal', 'cover', 'contain',
+].map(style => ({ name: style, label: style }))
+
const TEXT_FONT_FAMILIES = [
'sans-serif', 'serif', 'fantasy', 'monospace', 'cursive',
].map(style => ({ name: style, label: style }))
@@ -49,18 +56,38 @@ const TEXT_FONT_STYLES = [
].map(style => ({ name: style, label: style }))
const CURSORS = [
+ { name: 'none', label: 'None', },
{ name: 'hand_up', label: 'Up', },
{ name: 'hand_down', label: 'Down', },
{ name: 'hand_left', label: 'Left', },
{ name: 'hand_right', label: 'Right', },
+ { name: 'unclickable', label: 'Unclickable', },
+]
+
+const UNITS = [
+ { name: 'px', label: 'pixels' },
+ { name: '%', label: 'percent' },
+ { name: 'video', label: 'video' },
+ { name: 'vmin', label: 'screen min' },
+ { name: 'vmax', label: 'screen max' },
]
const NO_LINK = 0
const EXTERNAL_LINK = -1
+const OPEN_POPUP_LINK = -2
+const CLOSE_POPUP_LINK = -3
const PAGE_LIST_TOP_OPTIONS = [
{ name: NO_LINK, label: 'No link' },
{ name: EXTERNAL_LINK, label: 'External link' },
- { name: -2, label: '──────────', disabled: true },
+ { name: OPEN_POPUP_LINK, label: 'Open popup' },
+ { name: CLOSE_POPUP_LINK, label: 'Close popup' },
+ { name: -99, label: '──────────', disabled: true },
+]
+
+const NO_POPUP = 0
+const POPUP_LIST_TOP_OPTIONS = [
+ { name: NO_POPUP, label: 'Select a popup group' },
+ { name: -99, label: '──────────', disabled: true },
]
// target_page_id = Column(Integer, ForeignKey('page.id'), nullable=True)
@@ -76,7 +103,26 @@ const newImage = (data) => ({
cursor: 'hand_up',
},
type: 'image',
- target_page_id: null,
+ target_page_id: 0,
+ ...data,
+})
+
+const newVideo = (data) => ({
+ settings: {
+ ...newPosition(),
+ video_style: 'cover',
+ url: "",
+ external_link_url: "",
+ cursor: 'none',
+ muted: false,
+ loop_style: false,
+ autoadvance: false,
+ loop_section: false,
+ loop_start: 0,
+ loop_end: 0,
+ },
+ type: 'video',
+ target_page_id: 0,
...data,
})
@@ -91,11 +137,12 @@ const newText = (data) => ({
background_color: 'transparent',
width: 0,
height: 0,
+ units: 'px',
external_link_url: "",
cursor: 'hand_up',
},
type: 'text',
- target_page_id: null,
+ target_page_id: 0,
...data,
})
@@ -104,9 +151,10 @@ const newLink = (data) => ({
...newPosition({ width: 100, height: 100, }),
external_link_url: "",
cursor: 'hand_up',
+ units: 'px',
},
type: 'link',
- target_page_id: null,
+ target_page_id: 0,
...data,
})
@@ -123,12 +171,18 @@ const newPosition = (data) => ({
width: 0, height: 0,
rotation: 0, scale: 1,
opacity: 1,
+ units: false,
align: "center_center",
+ has_audio: false,
+ audio_on_click_id: 0,
+ audio_on_hover_id: 0,
+ navigate_when_audio_finishes: false,
...data,
})
const TYPE_CONSTRUCTORS = {
image: newImage,
+ video: newVideo,
text: newText,
link: newLink,
script: newScript,
@@ -141,6 +195,20 @@ class TileForm extends Component {
errorFields: new Set([]),
modified: false,
pageList: [],
+ popupList: [],
+ }
+
+ constructor(props){
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ this.handleSelect = this.handleSelect.bind(this)
+ this.handleSettingsChange = this.handleSettingsChange.bind(this)
+ this.handleSettingsSelect = this.handleSettingsSelect.bind(this)
+ this.handleAlignment = this.handleAlignment.bind(this)
+ this.handleImageChange = this.handleImageChange.bind(this)
+ this.handleVideoChange = this.handleVideoChange.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ this.handleDelete = this.handleDelete.bind(this)
}
componentDidMount() {
@@ -158,7 +226,11 @@ class TileForm extends Component {
...PAGE_LIST_TOP_OPTIONS,
...linkPages.map(page => ({ name: page.id, label: page.path }))
]
- this.setState({ pageList })
+ let popupList = [
+ ...POPUP_LIST_TOP_OPTIONS,
+ ...Object.keys(page.editor.popups).map(popup_group => ({ name: popup_group, label: popup_group }))
+ ]
+ this.setState({ pageList, popupList })
if (isNew) {
const newTile = newImage({
id: "new",
@@ -277,6 +349,24 @@ class TileForm extends Component {
})
}
+ handleVideoChange(e) {
+ const { name, value } = e.target
+ this.handleSettingsSelect(name, value)
+ preloadVideo(value).then(video => {
+ // console.log(img)
+ this.props.tileActions.updateTemporaryTile({
+ ...this.props.temporaryTile,
+ settings: {
+ ...this.props.temporaryTile.settings,
+ [name]: value,
+ width: video.videoWidth,
+ height: video.videoHeight,
+ x: 0, y: 0,
+ }
+ })
+ })
+ }
+
clearErrorField(name) {
const { errorFields } = this.state
if (errorFields.has(name)) {
@@ -313,6 +403,7 @@ class TileForm extends Component {
} else {
validData.id = temporaryTile.id
}
+ validData.target_page_id = validData.target_page_id || 0
this.setState({ modified: false })
console.log('submit', validData)
onSubmit(validData)
@@ -340,21 +431,24 @@ class TileForm extends Component {
return (
<div className='box'>
<h1>{title}</h1>
- <form onSubmit={this.handleSubmit.bind(this)}>
+ <button className='box_corner' onClick={this.props.pageActions.toggleSidebarSide}>
+ {'◁'}
+ </button>
+ <form onSubmit={this.handleSubmit}>
<div className="row selects">
<Select
name='type'
selected={temporaryTile.type}
options={SELECT_TYPES}
title=''
- onChange={this.handleSelect.bind(this)}
+ onChange={this.handleSelect}
/>
<Select
name='align'
selected={temporaryTile.settings.align}
options={ALIGNMENTS}
title=''
- onChange={this.handleAlignment.bind(this)}
+ onChange={this.handleAlignment}
/>
</div>
@@ -362,6 +456,8 @@ class TileForm extends Component {
{temporaryTile.type === 'image'
? this.renderImageForm()
+ : temporaryTile.type === 'video'
+ ? this.renderVideoForm()
: temporaryTile.type === 'text'
? this.renderTextForm()
: temporaryTile.type === 'link'
@@ -372,17 +468,18 @@ class TileForm extends Component {
{this.renderHyperlinkForm()}
{this.renderMiscForm()}
+ {this.renderAudioForm()}
<div className='row buttons'>
<SubmitButton
title={submitTitle}
- onClick={this.handleSubmit.bind(this)}
+ onClick={this.handleSubmit}
/>
{!isNew &&
<SubmitButton
title={'Delete'}
className='destroy'
- onClick={this.handleDelete.bind(this)}
+ onClick={this.handleDelete}
/>
}
</div>
@@ -427,7 +524,7 @@ class TileForm extends Component {
required
data={temporaryTile.settings}
error={errorFields.has('url')}
- onChange={this.handleImageChange.bind(this)}
+ onChange={this.handleImageChange}
autoComplete="off"
/>
</div>
@@ -436,7 +533,7 @@ class TileForm extends Component {
label="Tiled"
name="is_tiled"
checked={temporaryTile.settings.is_tiled}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
autoComplete="off"
/>
{temporaryTile.settings.is_tiled &&
@@ -445,7 +542,7 @@ class TileForm extends Component {
selected={temporaryTile.settings.tile_style || 'tile'}
options={IMAGE_TILE_STYLES}
title=''
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
}
</div>
@@ -453,6 +550,103 @@ class TileForm extends Component {
)
}
+ renderVideoForm() {
+ // const { isNew } = this.props
+ const { temporaryTile } = this.props
+ const { errorFields } = this.state
+ // console.log(temporaryTile.settings)
+ return (
+ <div>
+ <div className='row imageUrl'>
+ <TextInput
+ title=""
+ placeholder='http://'
+ name="url"
+ required
+ data={temporaryTile.settings}
+ error={errorFields.has('url')}
+ onChange={this.handleVideoChange}
+ autoComplete="off"
+ />
+ </div>
+ <div className='row pair with_checkbox'>
+ <Select
+ name='video_style'
+ selected={temporaryTile.settings.video_style || 'none'}
+ options={VIDEO_STYLES}
+ title=''
+ onChange={this.handleSettingsSelect}
+ />
+ <Checkbox
+ label="Loop"
+ name="loop"
+ checked={temporaryTile.settings.loop}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ </div>
+ <div className='row pair'>
+ <Checkbox
+ label="Muted"
+ name="muted"
+ className='short'
+ checked={temporaryTile.settings.muted}
+ onChange={this.handleSettingsSelect}
+ />
+ <Checkbox
+ label="Autoadvance"
+ name="autoadvance"
+ className='short'
+ checked={temporaryTile.settings.autoadvance}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ {!temporaryTile.settings.muted && (
+ <Slider
+ title='Volume'
+ name='volume'
+ value={('volume' in temporaryTile.settings) ? temporaryTile.settings.volume : 1.0}
+ onChange={this.handleSettingsSelect}
+ min={0.0}
+ max={1.0}
+ step={0.01}
+ />
+ )}
+ {temporaryTile.settings.loop && (
+ <div className='row'>
+ <Checkbox
+ label="Loop section?"
+ className='short'
+ name="loop_section"
+ checked={temporaryTile.settings.loop_section}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ )}
+ {temporaryTile.settings.loop && temporaryTile.settings.loop_section && (
+ <div className='row pair'>
+ <TextInput
+ title="From"
+ placeholder='0:00'
+ name="loop_start"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ <TextInput
+ title="To"
+ placeholder='0:00'
+ name="loop_end"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ </div>
+ )}
+ </div>
+ )
+ }
+
renderTextForm() {
const { temporaryTile } = this.props
const { errorFields } = this.state
@@ -464,7 +658,7 @@ class TileForm extends Component {
required
data={temporaryTile.settings}
error={errorFields.has('content')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<div className='row font'>
@@ -474,7 +668,7 @@ class TileForm extends Component {
selected={temporaryTile.settings.font_family || 'sans-serif'}
options={TEXT_FONT_FAMILIES}
title=''
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
<NumberInput
title=''
@@ -483,7 +677,7 @@ class TileForm extends Component {
min={1}
max={1200}
error={errorFields.has('font_size')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<Select
@@ -491,7 +685,7 @@ class TileForm extends Component {
selected={temporaryTile.settings.font_style || 'normal'}
options={TEXT_FONT_STYLES}
title=''
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
</div>
<ColorInput
@@ -499,7 +693,7 @@ class TileForm extends Component {
name='font_color'
data={temporaryTile.settings}
error={errorFields.has('font_color')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<ColorInput
@@ -507,7 +701,7 @@ class TileForm extends Component {
name='background_color'
data={temporaryTile.settings}
error={errorFields.has('background_color')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<div className='row pair'>
@@ -518,7 +712,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('width')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<NumberInput
@@ -528,7 +722,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('height')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
</div>
@@ -549,7 +743,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('width')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<NumberInput
@@ -559,7 +753,7 @@ class TileForm extends Component {
min={0}
max={1200}
error={errorFields.has('height')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
</div>
@@ -578,7 +772,7 @@ class TileForm extends Component {
required
data={temporaryTile.settings}
error={errorFields.has('content')}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
<div>
@@ -590,8 +784,12 @@ class TileForm extends Component {
renderHyperlinkForm() {
const { temporaryTile } = this.props
- const { pageList } = this.state
+ const { pageList, popupList } = this.state
const isExternalLink = temporaryTile.target_page_id === EXTERNAL_LINK
+ const isPopupLink = (
+ temporaryTile.target_page_id === OPEN_POPUP_LINK ||
+ temporaryTile.target_page_id === CLOSE_POPUP_LINK
+ )
return (
<div>
<div className={'row selects'}>
@@ -600,7 +798,7 @@ class TileForm extends Component {
name='target_page_id'
selected={temporaryTile.target_page_id || NO_LINK}
options={pageList}
- onChange={this.handleSelect.bind(this)}
+ onChange={this.handleSelect}
/>
<Select
title=''
@@ -608,21 +806,79 @@ class TileForm extends Component {
selected={temporaryTile.settings.cursor}
options={CURSORS}
defaultOption="Cursor"
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
/>
</div>
- <div>
- {isExternalLink &&
+ {isExternalLink && (
+ <div>
<TextInput
title=""
placeholder='http://'
name="external_link_url"
data={temporaryTile.settings}
- onChange={this.handleSettingsChange.bind(this)}
+ onChange={this.handleSettingsChange}
autoComplete="off"
/>
- }
- </div>
+ </div>
+ )}
+ {(temporaryTile.target_page_id === OPEN_POPUP_LINK || temporaryTile.target_page_id === CLOSE_POPUP_LINK) && (
+ <div className='row single'>
+ <Select
+ title="Popup"
+ name='target_popup'
+ selected={temporaryTile.settings.target_popup || NO_POPUP}
+ options={popupList}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ )}
+ </div>
+ )
+ }
+
+ renderAudioForm() {
+ const { temporaryTile } = this.props
+ return (
+ <div>
+ <Checkbox
+ label="Sound effects"
+ name="has_audio"
+ className='short'
+ checked={temporaryTile.settings.has_audio}
+ onChange={this.handleSettingsSelect}
+ />
+ {temporaryTile.settings.has_audio && (
+ <div>
+ <div className='row single'>
+ <AudioSelect
+ title="On click"
+ name="audio_on_click_id"
+ selected={temporaryTile.settings.audio_on_click_id}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+
+ {!!temporaryTile.settings.audio_on_click_id && (
+ <Checkbox
+ label="Navigate when audio finishes"
+ name="navigate_when_audio_finishes"
+ className='short'
+ checked={temporaryTile.settings.navigate_when_audio_finishes}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ )}
+
+ <div className='row single'>
+ <AudioSelect
+ title="On hover"
+ name="audio_on_hover_id"
+ selected={temporaryTile.settings.audio_on_hover_id}
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
+ </div>
+ )}
</div>
)
}
@@ -631,11 +887,20 @@ class TileForm extends Component {
const { temporaryTile } = this.props
return (
<div>
+ <div className='row single'>
+ <Select
+ name='units'
+ selected={temporaryTile.settings.units || 'px'}
+ options={UNITS}
+ title='Units'
+ onChange={this.handleSettingsSelect}
+ />
+ </div>
<Slider
title='Opacity'
name='opacity'
value={temporaryTile.settings.opacity}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
min={0.0}
max={1.0}
step={0.01}
@@ -644,7 +909,7 @@ class TileForm extends Component {
title='Scale'
name='scale'
value={temporaryTile.settings.scale}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
min={0.01}
max={10.0}
step={0.01}
@@ -653,12 +918,58 @@ class TileForm extends Component {
title='Rotation'
name='rotation'
value={temporaryTile.settings.rotation}
- onChange={this.handleSettingsSelect.bind(this)}
+ onChange={this.handleSettingsSelect}
min={-180.0}
max={180.0}
step={1}
type='int'
/>
+ <Checkbox
+ label="Element is a Popup"
+ name="is_popup"
+ className='short'
+ checked={temporaryTile.settings.is_popup}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ {temporaryTile.settings.is_popup && (
+ <div className='row single_text'>
+ <TextInput
+ title="Popup group"
+ name="popup_group"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ </div>
+ )}
+ <Checkbox
+ label="Wait to appear"
+ name="wait_to_appear"
+ className='short'
+ checked={temporaryTile.settings.wait_to_appear}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
+ {temporaryTile.settings.wait_to_appear && (
+ <div className='row single_text'>
+ <TextInput
+ title="Appear after"
+ name="appear_after"
+ data={temporaryTile.settings}
+ onChange={this.handleSettingsChange}
+ autoComplete="off"
+ />
+ </div>
+ )}
+ <Checkbox
+ label="Hide on click"
+ name="hide_on_click"
+ className='short'
+ checked={temporaryTile.settings.hide_on_click}
+ onChange={this.handleSettingsSelect}
+ autoComplete="off"
+ />
</div>
)
}
@@ -672,6 +983,7 @@ const mapStateToProps = state => ({
})
const mapDispatchToProps = dispatch => ({
+ pageActions: bindActionCreators({ ...pageActions }, dispatch),
tileActions: bindActionCreators({ ...tileActions }, dispatch),
})
diff --git a/frontend/app/views/tile/components/tile.handle.js b/frontend/app/views/tile/components/tile.handle.js
new file mode 100644
index 0000000..03b9f88
--- /dev/null
+++ b/frontend/app/views/tile/components/tile.handle.js
@@ -0,0 +1,8 @@
+import React from 'react'
+
+import handles from 'app/views/tile/handles'
+
+export default function TileHandle (props) {
+ const Tile = handles[props.tile.type]
+ return <Tile {...props} />
+}
diff --git a/frontend/app/views/page/components/tile.list.js b/frontend/app/views/tile/components/tile.list.js
index c455489..127ca52 100644
--- a/frontend/app/views/page/components/tile.list.js
+++ b/frontend/app/views/tile/components/tile.list.js
@@ -114,6 +114,10 @@ const TileListLink = ({ tile, pageTitles }) => (
{'Link: '}
{tile.target_page_id === -1
? 'External'
+ : tile.target_page_id === -2
+ ? 'Open popup'
+ : tile.target_page_id === -3
+ ? 'Close popup'
: !tile.target_page_id
? 'No link specified!'
: tile.target_page_id in pageTitles
@@ -123,6 +127,14 @@ const TileListLink = ({ tile, pageTitles }) => (
</div>
)
+const TileListVideo = ({ tile }) => {
+ return (
+ <div className='row' data-id={tile.id}>
+ <span className='snippet'>{"Vido: "}{tile.settings.url}</span>
+ </div>
+ )
+}
+
const TileListMisc = ({ tile }) => (
<div className='row' data-id={tile.id}>
<span className='snippet'>{"Tile: "}{tile.type}</span>
diff --git a/frontend/app/views/page/components/tile.new.js b/frontend/app/views/tile/components/tile.new.js
index b491fdd..e0f61a6 100644
--- a/frontend/app/views/page/components/tile.new.js
+++ b/frontend/app/views/tile/components/tile.new.js
@@ -21,6 +21,7 @@ class TileNew extends Component {
// history.push('/' + graph.path + '/' + res.res.path)
// }
this.props.pageActions.hideAddTileForm()
+ this.props.pageActions.loadPopups()
this.props.tileActions.clearTemporaryTile()
})
.catch(err => {
diff --git a/frontend/app/views/tile/handles/index.js b/frontend/app/views/tile/handles/index.js
new file mode 100644
index 0000000..8aaeb06
--- /dev/null
+++ b/frontend/app/views/tile/handles/index.js
@@ -0,0 +1,14 @@
+
+import TileImage from './tile.image'
+import TileVideo from './tile.video'
+import TileLink from './tile.link'
+import TileText from './tile.text'
+import TileScript from './tile.script'
+
+export default {
+ image: TileImage,
+ video: TileVideo,
+ link: TileLink,
+ text: TileText,
+ script: TileScript,
+}
diff --git a/frontend/app/views/tile/handles/tile.image.js b/frontend/app/views/tile/handles/tile.image.js
new file mode 100644
index 0000000..9ab5616
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.image.js
@@ -0,0 +1,56 @@
+import React from 'react'
+import { generateTransform } from 'app/views/tile/tile.utils'
+
+export default function TileImage({ tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick }) {
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let content
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+
+ if (!tile.settings.url) {
+ return null
+ }
+ if (tile.settings.is_tiled) {
+ style.backgroundImage = 'url(' + tile.settings.url + ')'
+ style.backgroundPosition = tile.settings.align.replace('_', ' ')
+ switch (tile.settings.tile_style) {
+ default:
+ case 'tile':
+ break
+ case 'cover':
+ style.backgroundSize = 'cover'
+ break
+ case 'contain':
+ style.backgroundSize = 'contain'
+ break
+ case 'contain no-repeat':
+ style.backgroundSize = 'contain'
+ style.backgroundRepeat = 'no-repeat'
+ break
+ }
+ className += ' is_tiled'
+ } else {
+ className += ' ' + tile.settings.align
+ content = <img src={tile.settings.url} />
+ }
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {content}
+ </div>
+ )
+}
diff --git a/frontend/app/views/tile/handles/tile.link.js b/frontend/app/views/tile/handles/tile.link.js
new file mode 100644
index 0000000..4dd4fd4
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.link.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import { generateTransform, unitsDimension } from 'app/views/tile/tile.utils'
+
+export default function TileScript({ tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick }) {
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+
+ let content = ""
+ className += ' ' + tile.settings.align
+ style.width = unitsDimension(tile, 'width', bounds, videoBounds)
+ style.height = unitsDimension(tile, 'height', bounds, videoBounds)
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {content}
+ </div>
+ )
+}
diff --git a/frontend/app/views/tile/handles/tile.script.js b/frontend/app/views/tile/handles/tile.script.js
new file mode 100644
index 0000000..e844adf
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.script.js
@@ -0,0 +1,36 @@
+import React, { Component } from 'react'
+import { generateTransform } from 'app/views/tile/tile.utils'
+
+export default class TileScript extends Component {
+ componentDidMount(){
+ const { viewing, tile } = this.props
+ if (viewing) {
+ eval(tile.settings.content)
+ }
+ }
+ render() {
+ if (viewing) {
+ return <div style={{ display: 'none' }} />
+ }
+
+ const { tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick } = this.props
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let className = ['tile', tile.type, 'hand_up'].join(' ')
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {"SCRIPT"}
+ </div>
+ )
+ }
+}
diff --git a/frontend/app/views/tile/handles/tile.text.js b/frontend/app/views/tile/handles/tile.text.js
new file mode 100644
index 0000000..2fd63b2
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.text.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import { generateTransform, unitsDimension } from 'app/views/tile/tile.utils'
+
+export default function TileScript({ tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick }) {
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ // console.log(generateTransform(tile))
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+
+ if (!tile.settings.content) {
+ return null
+ }
+
+ let content = <span dangerouslySetInnerHTML={{ __html: tile.settings.content }} />
+ className += ' ' + tile.settings.align
+ style.width = unitsDimension(tile, 'width', bounds, videoBounds)
+ style.height = unitsDimension(tile, 'height', bounds, videoBounds)
+ style.fontFamily = tile.settings.font_family
+ style.fontSize = tile.settings.font_size + 'px'
+ style.lineHeight = 1.5
+ style.fontWeight = (tile.settings.font_style || "").indexOf('bold') !== -1 ? 'bold' : 'normal'
+ style.fontStyle = (tile.settings.font_style || "").indexOf('italic') !== -1 ? 'italic' : 'normal'
+ style.backgroundColor = tile.settings.background_color || 'transparent'
+ style.color = tile.settings.font_color || '#dddddd!important'
+
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ {content}
+ </div>
+ )
+}
diff --git a/frontend/app/views/tile/handles/tile.video.js b/frontend/app/views/tile/handles/tile.video.js
new file mode 100644
index 0000000..3166848
--- /dev/null
+++ b/frontend/app/views/tile/handles/tile.video.js
@@ -0,0 +1,108 @@
+import React, { Component } from 'react'
+
+import { generateTransform, generateVideoStyle } from 'app/views/tile/tile.utils'
+import { timestampToSeconds } from 'app/utils'
+
+export default class TileVideo extends Component {
+ constructor(props) {
+ super(props)
+ this.videoRef = React.createRef()
+ this.handleTimeUpdate = this.handleTimeUpdate.bind(this)
+ this.handleEnded = this.handleEnded.bind(this)
+ }
+
+ componentDidMount() {
+ this.bind()
+ }
+
+ componentDidUpdate() {
+ this.unbind()
+ this.bind()
+ }
+
+ componentWillUnmount() {
+ this.unbind()
+ }
+
+ bind() {
+ if (!this.videoRef.current) return
+ this.el = this.videoRef.current
+ this.el.addEventListener('ended', this.handleEnded)
+ this.el.addEventListener('timeupdate', this.handleTimeUpdate)
+ const muted = this.props.viewing
+ ? this.props.tile.settings.muted
+ : true
+ const volume = muted
+ ? 0.0
+ : ('volume' in this.props.tile.settings)
+ ? this.props.tile.settings.volume
+ : 1.0
+ this.el.volume = volume
+ }
+
+ unbind() {
+ if (!this.el) return
+ this.el.removeEventListener('timeupdate', this.handleTimeUpdate)
+ this.el.removeEventListener('ended', this.handleEnded)
+ }
+
+ handleTimeUpdate() {
+ if (this.props.tile.settings.loop && this.props.tile.settings.loop_section) {
+ const loop_start = timestampToSeconds(this.props.tile.settings.loop_start) || 0
+ const loop_end = timestampToSeconds(this.props.tile.settings.loop_end) || this.videoRef.current.duration
+ if (this.videoRef.current.currentTime > loop_end) {
+ this.videoRef.current.currentTime = loop_start
+ }
+ }
+ }
+
+ handleEnded() {
+ this.props.onPlaybackEnded(this.props.tile)
+ if (this.props.tile.settings.loop && this.props.tile.settings.loop_section) {
+ const loop_start = timestampToSeconds(this.props.tile.settings.loop_start) || 0
+ this.videoRef.current.currentTime = loop_start
+ }
+ }
+
+ render() {
+ let { tile, box, bounds, videoBounds, viewing, onMouseDown, onDoubleClick } = this.props
+ // console.log(tile)
+ const style = {
+ transform: generateTransform(tile, box, bounds, videoBounds),
+ opacity: tile.settings.opacity,
+ }
+ let className = ['tile', tile.type].join(' ')
+ if (tile.target_page_id || (viewing && tile.href)) {
+ if (viewing || tile.settings.cursor !== 'unclickable') {
+ className += ' ' + (tile.settings.cursor || 'hand_up')
+ }
+ }
+ // console.log(tile.settings)
+ if (!tile.settings.url) {
+ return null
+ }
+ className += ' ' + tile.settings.align
+ const muted = viewing
+ ? tile.settings.muted
+ : true
+ return (
+ <div
+ className={className}
+ onMouseDown={onMouseDown}
+ onDoubleClick={onDoubleClick}
+ style={style}
+ >
+ <video
+ ref={this.videoRef}
+ src={tile.settings.url}
+ autoPlay={true}
+ controls={false}
+ disablePictureInPicture={true}
+ loop={tile.settings.loop}
+ muted={muted}
+ style={generateVideoStyle(tile, bounds)}
+ />
+ </div>
+ )
+ }
+}
diff --git a/frontend/app/views/tile/tile.utils.js b/frontend/app/views/tile/tile.utils.js
new file mode 100644
index 0000000..46d7764
--- /dev/null
+++ b/frontend/app/views/tile/tile.utils.js
@@ -0,0 +1,80 @@
+export const generateTransform = (tile, box, bounds, videoBounds) => {
+ let { x, y, align, rotation, scale, units, is_tiled } = tile.settings
+ if (is_tiled) {
+ return 'translateZ(0)'
+ }
+ if (box) {
+ x += box.dx
+ y += box.dy
+ }
+ units = units || 'px'
+ const [yalign, xalign] = align.split('_')
+ let transform = ['translateZ(0)']
+ if (yalign === 'center') {
+ transform.push('translateY(-50%)')
+ }
+ if (xalign === 'center') {
+ transform.push('translateX(-50%)')
+ }
+ // if (x % 2 == 1) x += 0.5
+ // if (y % 2 == 1) y += 0.5
+ const xUnits = units === 'video' ? videoUnits(x, bounds, videoBounds) : x + units
+ const yUnits = units === 'video' ? videoUnits(y, bounds, videoBounds) : y + units
+
+ transform.push('translateX(' + xUnits + ')')
+ transform.push('translateY(' + yUnits + ')')
+ if (scale !== 1) {
+ transform.push('scale(' + scale + ')')
+ }
+ if (rotation !== 0) {
+ transform.push('rotateZ(' + rotation + 'deg)')
+ }
+ return transform.join(' ')
+}
+
+export const generateVideoStyle = (tile, bounds) => {
+ const style = {
+ pointerEvents: "none",
+ }
+ switch (tile.settings.video_style) {
+ case 'normal':
+ style.width = "auto"
+ style.height = "auto"
+ break
+ case 'cover':
+ if (tile.settings.width && (tile.settings.width / tile.settings.height) > (bounds.width / bounds.height)) {
+ style.width = Math.round((tile.settings.width / tile.settings.height) * bounds.height)
+ style.height = bounds.height
+ } else {
+ style.width = bounds.width
+ style.height = Math.round((tile.settings.height / tile.settings.width) * bounds.width)
+ }
+ break
+ case 'contain':
+ if (tile.settings.width && (tile.settings.width / tile.settings.height) > (bounds.width / bounds.height)) {
+ style.width = bounds.width
+ style.height = Math.round((tile.settings.height / tile.settings.width) * bounds.width)
+ } else {
+ style.width = Math.round((tile.settings.width / tile.settings.height) * bounds.height)
+ style.height = bounds.height
+ }
+ break
+ }
+ return style
+}
+
+export const unitsDimension = (tile, dimension, bounds, videoBounds) => {
+ const value = tile.settings[dimension]
+ if (!value) return "auto"
+ if (tile.settings.units) {
+ if (tile.settings.units === 'video') {
+ return videoUnits(value, bounds, videoBounds)
+ }
+ return value + tile.settings.units
+ }
+ return value + "px"
+}
+
+export const videoUnits = (value, bounds, videoBounds) => (
+ Math.round(value / videoBounds.width * bounds.width) + 'px'
+) \ No newline at end of file