diff --git a/package.json b/package.json index 964c241..81dbc9c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@dabeng/react-orgchart": "^1.0.0", "@develoka/angka-rupiah-js": "^1.0.3", "@faker-js/faker": "^7.3.0", + "@grapecity/wijmo.react.all": "5.20213", "@iconify/icons-ant-design": "^1.0.6", "@iconify/icons-fa-solid": "^1.0.6", "@iconify/icons-fe": "^1.0.3", diff --git a/src/components/wj/App.jsx b/src/components/wj/App.jsx new file mode 100644 index 0000000..09b5099 --- /dev/null +++ b/src/components/wj/App.jsx @@ -0,0 +1,270 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcCore from '@grapecity/wijmo'; +import { Grid } from './components/Grid'; +import { RadialGauge } from './components/RadialGauge'; +import { LinearGauge } from './components/LinearGauge'; +import { BarChart } from './components/BarChart'; +import { ColumnChart } from './components/ColumnChart'; +import { LineChart } from './components/LineChart'; +import { BubbleChart } from './components/BubbleChart'; +import { BulletGraph } from './components/BulletGraph'; +import { Blank } from './components/Blank'; +import { Tile } from './components/Tile'; +// Wijmo and Material Design Lite +import '@grapecity/wijmo.styles/themes/material/wijmo.theme.material.indigo-amber.css'; +// Styles +import './index.css'; + +import './license' + +export default class App extends React.Component { + constructor() { + super(...arguments); + // Color palette + this.palette = ['#8e99f3', '#ffca28', '#5c6bc0', '#bbdefb']; + // Icons assets + this.icons = { + grid: [ + , + , + ], + barChart: [ + , + , + , + , + ], + columnChart: [ + , + , + , + , + ], + bubbleChart: [ + , + , + , + ], + lineChart: [ + , + , + , + , + ], + radialGauge: [ + , + , + , + ], + linearGauge: [ + , + , + , + ], + bulletGraph: [ + , + , + , + , + , + , + , + , + , + ], + blank: [ + , + ], + }; + // Tile names and types + this.tileCatalog = [ + { name: 'Grid', tile: Grid, icon: this.icons.grid }, + { name: 'Radial Gauge', tile: RadialGauge, icon: this.icons.radialGauge }, + { name: 'Linear Gauge', tile: LinearGauge, icon: this.icons.linearGauge }, + { name: 'Bar Chart', tile: BarChart, icon: this.icons.barChart }, + { name: 'Column Chart', tile: ColumnChart, icon: this.icons.columnChart }, + { name: 'Line Chart', tile: LineChart, icon: this.icons.lineChart }, + { name: 'Bubble Chart', tile: BubbleChart, icon: this.icons.bubbleChart }, + { name: 'Bullet Graph', tile: BulletGraph, icon: this.icons.bulletGraph }, + { name: 'Blank', tile: Blank, icon: this.icons.blank }, + ]; + this.key = 0; + this.state = { + isWideMenu: false, + tileCatalog: new wjcCore.CollectionView(this.tileCatalog), + tiles: this.getTiles(), + key: this.key, + data: this.getData(), + }; + } + // tiles currently in use + getTiles() { + return [ + { name: this.tileCatalog[1].name, key: this.key += 1 }, + { name: this.tileCatalog[2].name, key: this.key += 1 }, + { name: this.tileCatalog[5].name, key: this.key += 1 }, + { name: this.tileCatalog[7].name, key: this.key += 1 }, + { name: this.tileCatalog[0].name, key: this.key += 1 }, + ]; + } + // generate some data to show in the tiles + getData() { + let data = []; + const today = new Date(); + for (let i = 0; i < 12; i++) { + const sales = 100 + Math.random() * 800 + i * 50; + const expenses = 50 + Math.random() * 300 + i * 5; + data.push({ + id: i, + date: wjcCore.DateTime.addMonths(today, 12 - i), + sales: sales, + expenses: expenses, + profit: sales - expenses, + }); + } + return data; + } + // gets a tile content by name + getTileContent(name) { + const { data, tileCatalog } = this.state; + const arr = tileCatalog.items; + for (let i = 0; i < arr.length; i++) { + if (arr[i].name == name) { + return React.createElement(arr[i].tile, { + data: new wjcCore.CollectionView(data), + palette: this.palette + }); + } + } + throw '*** tile not found: ' + name; + } + // adds a tile to the dashboard + addTile(name) { + const { tiles, key: stateKey } = this.state; + const key = stateKey + 1; + this.setState({ tiles: [{ name, key }, ...tiles], key }); + } + // removes a tile from the dashboard + removeTile(tileIndex) { + const tiles = this.state.tiles.filter((item, index) => index != tileIndex); + this.setState({ tiles: tiles }); + } + // initialize component after it has been mounted + componentDidMount() { + // enable tile drag/drop + const panel = document.querySelector('.dashboard'); + this.enableItemReorder(panel); + } + // allow users to re-order elements within a panel element + // we work with the DOM elements and update the state when done. + enableItemReorder(panel) { + let dragSource = null; + let dropTarget = null; + // add drag/drop event listeners + panel.addEventListener('dragstart', (e) => { + const target = wjcCore.closest(e.target, '.tile'); + if (target && target.parentElement == panel) { + dragSource = target; + wjcCore.addClass(dragSource, 'drag-source'); + const dt = e.dataTransfer; + dt.effectAllowed = 'move'; + dt.setData('text', dragSource.innerHTML); + } + }); + panel.addEventListener('dragover', (e) => { + if (dragSource) { + let tile = wjcCore.closest(e.target, '.tile'); + if (tile == dragSource) { + tile = null; + } + if (dragSource && tile && tile != dragSource) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + } + if (dropTarget != tile) { + wjcCore.removeClass(dropTarget, 'drag-over'); + dropTarget = tile; + wjcCore.addClass(dropTarget, 'drag-over'); + } + } + }); + panel.addEventListener('drop', (e) => { + if (dragSource && dropTarget) { + // finish drag/drop + e.stopPropagation(); + e.stopImmediatePropagation(); + e.preventDefault(); + // re-order HTML elements (optional here, we're updating the state later) + const srcIndex = getIndex(dragSource); + const dstIndex = getIndex(dropTarget); + const refChild = srcIndex > dstIndex ? dropTarget : dropTarget.nextElementSibling; + dragSource.parentElement.insertBefore(dragSource, refChild); + // focus and view on the tile that was dragged + dragSource.focus(); + // update state + let tiles = this.state.tiles.slice(); + tiles.splice(srcIndex, 1); + tiles.splice(dstIndex, 0, this.state.tiles[srcIndex]); + this.setState({ tiles: tiles }); + } + }); + panel.addEventListener('dragend', () => { + wjcCore.removeClass(dragSource, 'drag-source'); + wjcCore.removeClass(dropTarget, 'drag-over'); + dragSource = dropTarget = null; + }); + function getIndex(e) { + const p = e.parentElement; + for (let i = 0; i < p.children.length; i++) { + if (p.children[i] == e) + return i; + } + return -1; + } + } + // render the dashboard + render() { + const { tiles, isWideMenu } = this.state; + // animated toggle menu + const renderMenuToggle = (
this.setState({ isWideMenu: !isWideMenu })}> + + + + + +
); + // menu items + const renderMenuItems = ( + {this.tileCatalog.map((item) => (
this.addTile(item.name)}> + + {item.icon.map((entity, key) => ({entity}))} + +
{item.name}
+
))} +
); + // displayed when the dashboard is empty + const renderBlankTile = (
+ + + +
Click on an item on the menu bar to add the new tile to the dashboard.
+
); + // list of tiles + const renderTiles = ( + {tiles.map((item, index) => ())} + ); + const renderDashboard = tiles.length ? renderTiles : renderBlankTile; + return (
+
+ {renderMenuToggle} + {renderMenuItems} +
+
+
+
{renderDashboard}
+
+
); + } +} diff --git a/src/components/wj/components/BarChart.jsx b/src/components/wj/components/BarChart.jsx new file mode 100644 index 0000000..a9dc664 --- /dev/null +++ b/src/components/wj/components/BarChart.jsx @@ -0,0 +1,11 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcChart from '@grapecity/wijmo.chart'; +import * as wjChart from '@grapecity/wijmo.react.chart'; +export const BarChart = ({ data, palette }) => ( + + + + + ); diff --git a/src/components/wj/components/Blank.jsx b/src/components/wj/components/Blank.jsx new file mode 100644 index 0000000..28f67ca --- /dev/null +++ b/src/components/wj/components/Blank.jsx @@ -0,0 +1,3 @@ +// React +import * as React from 'react'; +export const Blank = () =>
This is an empty tile.
; diff --git a/src/components/wj/components/BubbleChart.jsx b/src/components/wj/components/BubbleChart.jsx new file mode 100644 index 0000000..ad8f98d --- /dev/null +++ b/src/components/wj/components/BubbleChart.jsx @@ -0,0 +1,9 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcChart from '@grapecity/wijmo.chart'; +import * as wjChart from '@grapecity/wijmo.react.chart'; +export const BubbleChart = ({ data, palette }) => ( + + + ); diff --git a/src/components/wj/components/BulletGraph.jsx b/src/components/wj/components/BulletGraph.jsx new file mode 100644 index 0000000..ed7af7a --- /dev/null +++ b/src/components/wj/components/BulletGraph.jsx @@ -0,0 +1,22 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcCore from '@grapecity/wijmo'; +import * as wjGauge from '@grapecity/wijmo.react.gauge'; +export const BulletGraph = ({ data }) => (
+ + + {data.items.map((item, index) => ( + + + + ))} + +
{wjcCore.Globalize.format(item.date, 'MMM yyyy')} + + + + + +
+
); diff --git a/src/components/wj/components/ColumnChart.jsx b/src/components/wj/components/ColumnChart.jsx new file mode 100644 index 0000000..5d8e495 --- /dev/null +++ b/src/components/wj/components/ColumnChart.jsx @@ -0,0 +1,11 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcChart from '@grapecity/wijmo.chart'; +import * as wjChart from '@grapecity/wijmo.react.chart'; +export const ColumnChart = ({ data, palette }) => ( + + + + + ); diff --git a/src/components/wj/components/Grid.jsx b/src/components/wj/components/Grid.jsx new file mode 100644 index 0000000..1d0caef --- /dev/null +++ b/src/components/wj/components/Grid.jsx @@ -0,0 +1,12 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcGrid from '@grapecity/wijmo.grid'; +import * as wjGrid from '@grapecity/wijmo.react.grid'; +export const Grid = ({ data, palette }) => ( + + + + + + ); diff --git a/src/components/wj/components/LineChart.jsx b/src/components/wj/components/LineChart.jsx new file mode 100644 index 0000000..6f93ab2 --- /dev/null +++ b/src/components/wj/components/LineChart.jsx @@ -0,0 +1,10 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcChart from '@grapecity/wijmo.chart'; +import * as wjChart from '@grapecity/wijmo.react.chart'; +export const LineChart = ({ data, palette }) => ( + + + + ); diff --git a/src/components/wj/components/LinearGauge.jsx b/src/components/wj/components/LinearGauge.jsx new file mode 100644 index 0000000..3a14ec3 --- /dev/null +++ b/src/components/wj/components/LinearGauge.jsx @@ -0,0 +1,26 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcCore from '@grapecity/wijmo'; +import * as wjGauge from '@grapecity/wijmo.react.gauge'; +export const LinearGauge = ({ data, palette }) => { + const lastItem = data.items[data.items.length - 1]; + return (
+
+

Sales: {wjcCore.Globalize.format(lastItem.sales, 'c')}

+ +
+ +
+

Expenses: {wjcCore.Globalize.format(lastItem.expenses, 'c')}

+ +
+ +
+

Profit: {wjcCore.Globalize.format(lastItem.profit, 'c')}

+ +
+ +

KPIs for {wjcCore.Globalize.format(lastItem.date, 'MMMM yyyy')}

+
); +}; diff --git a/src/components/wj/components/RadialGauge.jsx b/src/components/wj/components/RadialGauge.jsx new file mode 100644 index 0000000..00faeb9 --- /dev/null +++ b/src/components/wj/components/RadialGauge.jsx @@ -0,0 +1,12 @@ +// React +import * as React from 'react'; +// Wijmo +import * as wjcCore from '@grapecity/wijmo'; +import * as wjGauge from '@grapecity/wijmo.react.gauge'; +export const RadialGauge = ({ data, palette }) => { + const lastItem = data.items[data.items.length - 1]; + return ( + +

Profit for {wjcCore.Globalize.format(lastItem.date, 'MMMM yyyy')}

+
); +}; diff --git a/src/components/wj/components/Tile.jsx b/src/components/wj/components/Tile.jsx new file mode 100644 index 0000000..62cdcf5 --- /dev/null +++ b/src/components/wj/components/Tile.jsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +export const Tile = ({ header, content, index, onRemove }) => (
+
+
{header}
+
+
onRemove(index)}> + + + +
+
+
+
{content}
+
); diff --git a/src/components/wj/index.css b/src/components/wj/index.css new file mode 100644 index 0000000..87262fc --- /dev/null +++ b/src/components/wj/index.css @@ -0,0 +1,394 @@ +/* app */ +*, *::before, *::after { + box-sizing: border-box; +} + +/* customize the browser's scrollbar: */ +*::-webkit-scrollbar { + width: 6px; + height: 6px; +} +*::-webkit-scrollbar-track { + border-radius: 0.25rem; + background: rgba(0, 0, 0, 0.1); +} +*::-webkit-scrollbar-thumb { + border-radius: 0.25rem; + background: rgba(0, 0, 0, 0.2); +} +*::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.4); +} +*::-webkit-scrollbar-thumb:active { + background: rgba(0, 0, 0, 0.9); +} + +html, +body { + height: 100%; +} + +body { + background-color: #f7faff; + font-size: 0.875rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', sans-serif; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; +} + +h3 { + font-size: 0.875rem; + font-weight: 500; + text-align: center; +} + +h4 { + font-size: 0.875rem; + font-size: 0.75rem; + font-weight: 400; + min-width: 7rem; +} + +.flex-row { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 1rem; +} + +.button { + cursor: pointer; +} + +#app { + flex: 1 1 auto; + overflow: hidden; +} + +.container { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + max-width: 100%; + background: #f2f6fe; +} + +.hr { + width: 1px; + height: 100%; + position: relative; + background: #e4ecfb; + z-index: 1; +} +.hr::before { + content: ''; + display: block; + width: 1px; + height: 100%; + background-color: #f6f9fe; +} + +.blank { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; + justify-content: center; + text-align: center; +} + +.blank svg { + width: 3rem; + height: 3rem; + margin-bottom: 1rem; +} + +.content { + flex: 1 1 auto; + overflow: auto; + background-color: #f7faff; + width: 100%; + padding: 2rem; + box-sizing: border-box; +} +@media only screen and (max-width: 811px) { + .content { + padding: 0.25rem; + } +} + +.menu { + display: flex; + flex-direction: column; + font-size: 0.85rem; + justify-content: flex-start; + flex: 0 0 auto; + padding: 0; +} +@media only screen and (max-width: 840px), (max-height: 840px) { + .menu { + overflow: auto; + font: 0.75rem; + } +} +.menu.menu--open .menu-item-name { + display: block; +} + +.menu-toggle { + transition: all 500ms; + padding: 1rem; + text-align: center; + border-bottom: 1px solid #e6ecf1; + cursor: pointer; +} +.menu-toggle svg { + transition: all 250ms ease-out 50ms; + transform-origin: center center; + transform: scaleX(1); +} +.menu.menu--open .menu-toggle svg { + transform: scaleX(-1); +} + +.menu-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.5rem 1rem; + cursor: pointer; + user-select: none; + position: relative; + text-align: left; +} +.menu-item:hover { + background: #f7faff; +} +.menu-item:hover:before { + content: ''; + display: inline-block; + width: 1rem; + height: 1rem; + bottom: 2rem; + left: 2.5rem; + position: absolute; + background: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff), #0085c7; + background-position: center; + background-size: 50% 2px, 2px 50%; /*thickness = 2px, length = 50% (25px)*/ + background-repeat: no-repeat; + border-radius: 50%; + box-shadow: 0 1px 2px rgba(55, 63, 66, 0.07), 0 2px 4px rgba(55, 63, 66, 0.07), 0 4px 8px rgba(55, 63, 66, 0.07), + 0 8px 16px rgba(55, 63, 66, 0.07), 0 16px 24px rgba(55, 63, 66, 0.07), 0 24px 32px rgba(55, 63, 66, 0.07); +} +.menu-item-name { + margin: 0 0.5rem 0 1rem; + white-space: nowrap; + display: none; +} +@media only screen and (max-width: 840px), (max-height: 840px) { + .menu-toggle { + padding: 1rem 0; + } + .menu-item { + padding: 0.5rem; + } + .menu-item:hover:before { + bottom: 1rem; + left: 1rem; + } + .menu-item svg { + width: 32px; + height: 32px; + } +} + +/* dashboard and tiles */ +.dashboard { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin: auto; + height: 100%; +} + +.table { + margin: 0; + table-layout: fixed; + width: 100%; +} + +.table td { + padding: 0.15rem 0.5rem; + font-size: 0.75rem; + white-space: nowrap; + width: 1.5rem; +} + +.table td:first-child { + width: 4rem; +} + +.table td:last-child { + width: auto; +} + +.tile { + display: flex; + flex-direction: column; + flex: 0 0 auto; + width: calc(25% - 1rem); + margin: 0.5rem; + height: 50vh; + max-height: calc(50% - 1rem); + overflow: hidden; + background: white; + page-break-inside: avoid; /* important when printing the dashboard */ + transition: all 250ms; + border-radius: 0.5rem; + box-sizing: border-box; + box-shadow: 0 1px 2px rgba(55, 63, 66, 0.07), 0 2px 4px rgba(55, 63, 66, 0.07), 0 4px 8px rgba(55, 63, 66, 0.07), + 0 8px 16px rgba(55, 63, 66, 0.07), 0 16px 24px rgba(55, 63, 66, 0.07), 0 24px 32px rgba(55, 63, 66, 0.07); +} +@media only screen and (max-width: 1599px) { + .tile { + width: calc(33.33% - 1rem); + } +} +@media only screen and (max-width: 1079px) { + .tile { + width: calc(50% - 1rem); + } +} +@media only screen and (max-width: 1023px) { + .tile { + width: calc(100% - 1rem); + } +} +@media only screen and (max-height: 800px) { + .tile { + max-height: 400px; + } +} +.tile:last-child { + flex: 1 1 auto; +} +.tile:hover { + border-color: #adb7bd; +} +.tile.drag-over { + border: 2px dashed #000; +} + +.tile .buttons { + transition: all 250ms; + opacity: 0; +} +@media (hover: none) and (pointer: coarse) { + .tile .buttons { + opacity: 1; + } +} +.tile:hover .buttons { + opacity: 1; +} +.tile .buttons > span { + padding: 0 0.5rem; + cursor: pointer; +} +.tile.drag-over { + border: 2px dashed #000; + background-color: rgba(0, 0, 0, 0.1); + transition: all 250ms; +} +.tile.drag-source { + opacity: 0.4; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); + background-color: rgba(145, 200, 248, 0.75); + transform: scale(0.9); + transition: all 250ms; +} + +.tile .tile-container { + border-bottom: 1px solid #e0e0e0; + padding: 0.75rem 1rem; + display: flex; + cursor: move; +} + +.tile .tile-header { + flex-grow: 1; + font-size: 1rem; + font-weight: 400; + padding: 0.125rem; + opacity: 0.75; +} + +.tile .tile-content { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + flex: 1 1 auto; + overflow: auto; + height: 100%; +} + +.tile .blank-tile { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* tile content */ + +.wj-flexgrid { + border: none; + height: 100%; +} + +.wj-flexgrid .wj-cell { + border-right: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 0.35rem 1rem; + font-size: 0.8125rem; +} + +.wj-flexchart { + background: transparent; + height: calc(100%); + width: 100%; + border: none; + padding: 1rem; + margin: 0; + overflow: hidden; +} +.wj-radialgauge { + width: 60%; + max-width: 300px; + padding: 1rem; + overflow: hidden; +} +.wj-radialgauge .wj-value { + font-size: 0.75rem; + font-weight: 500; +} + +.wj-lineargauge { + max-height: 1rem; + width: 100%; + overflow: hidden; +} + +.wj-gauge .wj-face path { + stroke: none; +} + +.wj-ranges { + opacity: 0.15; +} diff --git a/src/components/wj/index.jsx b/src/components/wj/index.jsx new file mode 100644 index 0000000..4f7df64 --- /dev/null +++ b/src/components/wj/index.jsx @@ -0,0 +1,10 @@ +import './license'; +// Wijmo and Material Design Lite +import '@grapecity/wijmo.styles/themes/material/wijmo.theme.material.indigo-amber.css'; +// Styles +import './index.css'; +//React +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { App } from './App'; +ReactDOM.render(React.createElement(App), document.getElementById('app')); diff --git a/src/components/wj/license.js b/src/components/wj/license.js new file mode 100644 index 0000000..5c66de4 --- /dev/null +++ b/src/components/wj/license.js @@ -0,0 +1,2 @@ +import { setLicenseKey } from '@grapecity/wijmo'; +setLicenseKey('GrapeCity,427351286422477#B0JoIIklkIs4nIzYXMyAjMiojIyVmdiwSZzxWYmpjIyNHZisnOiwmbBJye0ICRiwiI34TQvAVW7ZWbqNjeWZzRh9Ucl5UTuZVaCR7ZpB5UH9EVC3kaqJWZ0pnasJ7Q9I4bB3GR0F6aIt6NZ96MFN4aotEZrUXe4kHUlllerlGb9dDSPhEcFFmclJXd8syNEtENzMTOBNVYpFje6lDUlBlbkdmcNNGOrAHR9pXNSl6NpVkbCNWUxlkZ7o7QT3icHNGavFXdapWQDZ5KsN5N7dDcyRUW85ESFBlb4JUZq3GWlBldXBzU0hjW9wkb8cncopnSOBjNkJFdyNmSqB7YVlnNzpnb9BDSMllTSFDaNFjca54dtBXatlEdS5mVNV6dxIEN7RUNxYGSvU7KIdndPpENvlXW6hzbCh7RsZ7YLJzboNnU7ZERTRkVBNlN7RWSGhDVrdmZqZ4cq94aFpkbWZDMGBHdVZ5YwUTTIdlN0RnVvMDdFJzbQt6bolDM5NkdrsUO536LrNWSotmI0IyUiwiIDRTR4MjM9QjI0ICSiwSMyIjM9UTMwEjM0IicfJye35XX3JSSwIjUiojIDJCLi86bpNnblRHeFBCI4VWZoNFelxmRg2Wbql6ViojIOJyes4nI5kkTRJiOiMkIsIibvl6cuVGd8VEIgIXZ7VWaWRncvBXZSBybtpWaXJiOi8kI1xSfis4N8gkI0IyQiwiIu3Waz9WZ4hXRgAydvJVa4xWdNBybtpWaXJiOi8kI1xSfiQjR6QkI0IyQiwiIu3Waz9WZ4hXRgACUBx4TgAybtpWaXJiOi8kI1xSfiMzQwIkI0IyQiwiIlJ7bDBybtpWaXJiOi8kI1xSfiUFO7EkI0IyQiwiIu3Waz9WZ4hXRgACdyFGaDxWYpNmbh9WaGBybtpWaXJiOi8kI1tlOiQmcQJCLiETMwAzNwACOwcDMwIDMyIiOiQncDJCLi46bj9idlRWe4l6YlBXYydmLqwibj9SbvNmL9RXajVGchJ7ZuoCLt36YukHdpNWZwFmcn9iKsI7au26YukHdpNWZwFmcn9iKsAnau26YukHdpNWZwFmcn9iKiojIz5GRiwiI9RXaDVGchJ7RiojIh94QiwiI7cDNyIDN6gjMxUzMdI6N'); diff --git a/src/components/wj/utils/DragDropTouch.js b/src/components/wj/utils/DragDropTouch.js new file mode 100644 index 0000000..dfcdbe7 --- /dev/null +++ b/src/components/wj/utils/DragDropTouch.js @@ -0,0 +1,370 @@ +import * as wjcCore from '@grapecity/wijmo'; +/** + * Object used to hold the data that is being dragged during drag and drop operations. + * + * It may hold one or more data items of different types. For more information about + * drag and drop operations and data transfer objects, see + * HTML Drag and Drop API. + * + * This object is created automatically by the @see:DragDropTouch singleton and is + * accessible through the @see:dataTransfer property of all drag events. + */ +export class DataTransfer { + constructor() { + this._dropEffect = 'move'; + this._effectAllowed = 'all'; + this._data = {}; + } + /** + * Gets or sets the type of drag-and-drop operation currently selected. + * The value must be 'none', 'copy', 'link', or 'move'. + */ + get dropEffect() { + return this._dropEffect; + } + set dropEffect(value) { + this._dropEffect = wjcCore.asString(value); + } + /** + * Gets or sets the types of operations that are possible. + * Must be one of 'none', 'copy', 'copyLink', 'copyMove', 'link', + * 'linkMove', 'move', 'all' or 'uninitialized'. + */ + get effectAllowed() { + return this._effectAllowed; + } + set effectAllowed(value) { + this._effectAllowed = wjcCore.asString(value); + } + /** + * Gets an array of strings giving the formats that were set in the @see:dragstart event. + */ + get types() { + return Object.keys(this._data); + } + /** + * Removes the data associated with a given type. + * + * The type argument is optional. If the type is empty or not specified, the data + * associated with all types is removed. If data for the specified type does not exist, + * or the data transfer contains no data, this method will have no effect. + * + * @param type Type of data to remove. + */ + clearData(type) { + if (type != null) { + delete this._data[type]; + } + else { + this._data = null; + } + } + /** + * Retrieves the data for a given type, or an empty string if data for that type does + * not exist or the data transfer contains no data. + * + * @param type Type of data to retrieve. + */ + getData(type) { + return this._data[type] || ''; + } + /** + * Set the data for a given type. + * + * For a list of recommended drag types, please see + * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Recommended_Drag_Types. + * + * @param type Type of data to add. + * @param value Data to add. + */ + setData(type, value) { + this._data[type] = value; + } + /** + * Set the image to be used for dragging if a custom one is desired. + * + * @param img An image element to use as the drag feedback image. + * @param offsetX The horizontal offset within the image. + * @param offsetY The vertical offset within the image. + */ + setDragImage(img, offsetX, offsetY) { + var ddt = DragDropTouch._instance; + ddt._imgCustom = img; + ddt._imgOffset = new wjcCore.Point(offsetX, offsetY); + } +} +/** + * Defines a class that adds support for touch-based HTML5 drag/drop operations. + * + * The @see:DragDropTouch class listens to touch events and raises the + * appropriate HTML5 drag/drop events as if the events had been caused + * by mouse actions. + * + * The purpose of this class is to enable using existing, standard HTML5 + * drag/drop code on mobile devices running IOS or Android. + * + * To use, include the DragDropTouch.js file on the page. The class will + * automatically start monitoring touch events and will raise the HTML5 + * drag drop events (dragstart, dragenter, dragleave, drop, dragend) which + * should be handled by the application. + * + * For details and examples on HTML drag and drop, see + * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations. + */ +export class DragDropTouch { + /** + * Initializes the single instance of the @see:DragDropTouch class. + */ + constructor() { + this._lastClick = 0; + // enforce singleton pattern + wjcCore.assert(!DragDropTouch._instance, 'DragDropTouch instance already created.'); + // listen to touch events + if ('ontouchstart' in document) { + var d = document, ts = this._touchstart.bind(this), tm = this._touchmove.bind(this), te = this._touchend.bind(this); + d.addEventListener('touchstart', ts); + d.addEventListener('touchmove', tm); + d.addEventListener('touchend', te); + d.addEventListener('touchcancel', te); + } + } + /** + * Gets a reference to the @see:DragDropTouch singleton. + */ + static getInstance() { + return DragDropTouch._instance; + } + // ** event handlers + _touchstart(e) { + if (this._shouldHandle(e)) { + // raise double-click and prevent zooming + if (Date.now() - this._lastClick < DragDropTouch._DBLCLICK) { + if (this._dispatchEvent(e, 'dblclick', e.target)) { + e.preventDefault(); + this._reset(); + return; + } + } + // clear all variables + this._reset(); + // get nearest draggable element + var src = wjcCore.closest(e.target, '[draggable]'); + if (src) { + // give caller a chance to handle the hover/move events + if (!this._dispatchEvent(e, 'mousemove', e.target) && + !this._dispatchEvent(e, 'mousedown', e.target)) { + // get ready to start dragging + this._dragSource = src; + this._ptDown = this._getPoint(e); + this._lastTouch = e; + e.preventDefault(); + // show context menu if the user hasn't started dragging after a while + setTimeout(() => { + if (this._dragSource == src && this._img == null) { + if (this._dispatchEvent(e, 'contextmenu', src)) { + this._reset(); + } + } + }, DragDropTouch._CTXMENU); + } + } + } + } + _touchmove(e) { + if (this._shouldHandle(e)) { + // see if target wants to handle move + var target = this._getTarget(e); + if (this._dispatchEvent(e, 'mousemove', target)) { + this._lastTouch = e; + e.preventDefault(); + return; + } + // start dragging + if (this._dragSource && !this._img) { + var delta = this._getDelta(e); + if (delta > DragDropTouch._THRESHOLD) { + this._dispatchEvent(e, 'dragstart', this._dragSource); + this._createImage(e); + this._dispatchEvent(e, 'dragenter', target); + } + } + // continue dragging + if (this._img) { + this._lastTouch = e; + e.preventDefault(); // prevent scrolling + if (target != this._lastTarget) { + this._dispatchEvent(this._lastTouch, 'dragleave', this._lastTarget); + this._dispatchEvent(e, 'dragenter', target); + this._lastTarget = target; + } + this._moveImage(e); + this._dispatchEvent(e, 'dragover', target); + } + } + } + _touchend(e) { + if (this._shouldHandle(e)) { + // see if target wants to handle up + if (this._dispatchEvent(this._lastTouch, 'mouseup', e.target)) { + e.preventDefault(); + return; + } + // user clicked the element but didn't drag, so clear the source and simulate a click + if (!this._img) { + this._dragSource = null; + this._dispatchEvent(this._lastTouch, 'click', e.target); + this._lastClick = Date.now(); + } + // finish dragging + this._destroyImage(); + if (this._dragSource) { + if (e.type.indexOf('cancel') < 0) { + this._dispatchEvent(this._lastTouch, 'drop', this._lastTarget); + } + this._dispatchEvent(this._lastTouch, 'dragend', this._dragSource); + this._reset(); + } + } + } + // ** utilities + // ignore events that have been handled or that involve more than one touch + _shouldHandle(e) { + return e && + !e.defaultPrevented && + e.touches && e.touches.length < 2; + } + // clear all members + _reset() { + this._destroyImage(); + this._dragSource = null; + this._lastTouch = null; + this._lastTarget = null; + this._ptDown = null; + this._dataTransfer = new DataTransfer(); + } + // get point for a touch event + _getPoint(e, page) { + if (e && e.touches) { + e = e.touches[0]; + } + wjcCore.assert(e && ('clientX' in e), 'invalid event?'); + if (page == true) { + return new wjcCore.Point(e.pageX, e.pageY); + } + else { + return new wjcCore.Point(e.clientX, e.clientY); + } + } + // get distance between the current touch event and the first one + _getDelta(e) { + var p = this._getPoint(e); + return Math.abs(p.x - this._ptDown.x) + Math.abs(p.y - this._ptDown.y); + } + // get the element at a given touch event + _getTarget(e) { + var pt = this._getPoint(e), el = document.elementFromPoint(pt.x, pt.y); + while (el && getComputedStyle(el).pointerEvents == 'none') { + el = el.parentElement; + } + return el; + } + // create drag image from source element + _createImage(e) { + // just in case... + if (this._img) { + this._destroyImage(); + } + // create drag image from custom element or drag source + var src = this._imgCustom || this._dragSource; + this._img = src.cloneNode(true); + this._copyStyle(src, this._img); + this._img.style.top = this._img.style.left = '-9999px'; + // if creating from drag source, apply offset and opacity + if (!this._imgCustom) { + var rc = src.getBoundingClientRect(), pt = this._getPoint(e); + this._imgOffset = new wjcCore.Point(pt.x - rc.left, pt.y - rc.top); + this._img.style.opacity = DragDropTouch._OPACITY.toString(); + } + // add image to document + this._moveImage(e); + document.body.appendChild(this._img); + } + // dispose of drag image element + _destroyImage() { + if (this._img && this._img.parentElement) { + this._img.parentElement.removeChild(this._img); + } + this._img = null; + this._imgCustom = null; + } + // move the drag image element + _moveImage(e) { + requestAnimationFrame(() => { + var pt = this._getPoint(e, true); + wjcCore.setCss(this._img, { + position: 'absolute', + pointerEvents: 'none', + zIndex: 999999, + left: Math.round(pt.x - this._imgOffset.x), + top: Math.round(pt.y - this._imgOffset.y) + }); + }); + } + // copy properties from an object to another + _copyProps(dst, src, props) { + for (var i = 0; i < props.length; i++) { + var p = props[i]; + dst[p] = src[p]; + } + } + _copyStyle(src, dst) { + // remove potentially troublesome attributes + DragDropTouch._rmvAtts.forEach(function (att) { + dst.removeAttribute(att); + }); + // copy canvas content + if (src instanceof HTMLCanvasElement) { + var cSrc = src, cDst = dst; + cDst.width = cSrc.width; + cDst.height = cSrc.height; + cDst.getContext('2d').drawImage(cSrc, 0, 0); + } + // copy style + var cs = getComputedStyle(src); + for (var i = 0; i < cs.length; i++) { + var key = cs[i]; + dst.style[key] = cs[key]; + } + dst.style.pointerEvents = 'none'; + // and repeat for all children + for (var i = 0; i < src.children.length; i++) { + this._copyStyle(src.children[i], dst.children[i]); + } + } + _dispatchEvent(e, type, target) { + if (e && target) { + var evt = document.createEvent('Event'), t = e.touches ? e.touches[0] : e; + evt.initEvent(type, true, true); + evt.button = 0; + evt.which = evt.buttons = 1; + this._copyProps(evt, e, DragDropTouch._kbdProps); + this._copyProps(evt, t, DragDropTouch._ptProps); + evt.dataTransfer = this._dataTransfer; + target.dispatchEvent(evt); + return evt.defaultPrevented; + } + return false; + } +} +/*private*/ DragDropTouch._instance = new DragDropTouch(); // singleton +// constants +DragDropTouch._THRESHOLD = 5; // pixels to move before drag starts +DragDropTouch._OPACITY = 0.5; // drag image opacity +DragDropTouch._DBLCLICK = 500; // max ms between clicks in a double click +DragDropTouch._CTXMENU = 900; // ms to hold before raising 'contextmenu' event +// copy styles/attributes from drag source to drag image element +DragDropTouch._rmvAtts = 'id,class,style,draggable'.split(','); +// synthesize and dispatch an event +// returns true if the event has been handled (e.preventDefault == true) +DragDropTouch._kbdProps = 'altKey,ctrlKey,metaKey,shiftKey'.split(','); +DragDropTouch._ptProps = 'pageX,pageY,clientX,clientY,screenX,screenY'.split(','); diff --git a/src/routes.js b/src/routes.js index eb71b0f..1ede7b1 100644 --- a/src/routes.js +++ b/src/routes.js @@ -50,6 +50,7 @@ const UserShift = React.lazy(() => import('./views/SimproV2/UserShift')); const Kanban = React.lazy(() => import('./views/SimproV2/Kanban')); // const DashboardProject = React.lazy(() => import('./views/DashboardProject')); const DashboardBOD = React.lazy(() => import('./views/Dashboard/DashboardBOD')); +const DashboardDND = React.lazy(() => import('./components/wj/App')) const DashboardCustomer = React.lazy(() => import('./views/Dashboard/DashboardCustomer')); const DashboardProject = React.lazy(() => import('./views/Dashboard/DashboardProject')); const DashboardProjectCarousell = React.lazy(() => import('./views/Dashboard/DashboardProjectCarousell')); @@ -60,6 +61,7 @@ const DemoManagement = React.lazy(() => import('./views/SimproV2/Demo')) const routes = [ { path: '/', exact: true, name: 'Home' }, { path: '/dashboard', name: 'DashboardBOD', component: DashboardBOD }, + { path: '/dashboard-dnd', name: 'DashboardBOD', component: DashboardDND }, { path: '/dashboard-customer/:PROJECT_ID/:GANTT_ID/:SCURVE', name: 'DashboardCustomer', component: DashboardCustomer }, { path: '/dashboard-project/:PROJECT_ID/:GANTT_ID/:Header', exact: true, name: 'Dashboard Project', component: DashboardProject }, { path: '/dashboard-perproject', exact: true, name: 'Dashboard Project Carousell', component: DashboardProjectCarousell },