DashboardCard.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. /*
  2. * Copyright (C) 2015 - present Instructure, Inc.
  3. *
  4. * This file is part of Canvas.
  5. *
  6. * Canvas is free software: you can redistribute it and/or modify it under
  7. * the terms of the GNU Affero General Public License as published by the Free
  8. * Software Foundation, version 3 of the License.
  9. *
  10. * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
  11. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  13. * details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License along
  16. * with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. import _ from 'underscore'
  19. import React, { Component } from 'react'
  20. import PropTypes from 'prop-types'
  21. import I18n from 'i18n!dashcards'
  22. import DashboardCardAction from './DashboardCardAction'
  23. import CourseActivitySummaryStore from './CourseActivitySummaryStore'
  24. import DashboardCardMenu from './DashboardCardMenu'
  25. export default class DashboardCard extends Component {
  26. // ===============
  27. // CONFIG
  28. // ===============
  29. static propTypes = {
  30. id: PropTypes.string.isRequired,
  31. backgroundColor: PropTypes.string,
  32. shortName: PropTypes.string.isRequired,
  33. originalName: PropTypes.string.isRequired,
  34. courseCode: PropTypes.string.isRequired,
  35. assetString: PropTypes.string.isRequired,
  36. term: PropTypes.string,
  37. href: PropTypes.string.isRequired,
  38. links: PropTypes.arrayOf(PropTypes.object),
  39. imagesEnabled: PropTypes.bool,
  40. image: PropTypes.string,
  41. handleColorChange: PropTypes.func,
  42. hideColorOverlays: PropTypes.bool,
  43. reorderingEnabled: PropTypes.bool,
  44. isDragging: PropTypes.bool,
  45. connectDragSource: PropTypes.func,
  46. connectDropTarget: PropTypes.func,
  47. moveCard: PropTypes.func,
  48. totalCards: PropTypes.number,
  49. position: PropTypes.oneOfType([PropTypes.number, PropTypes.func])
  50. }
  51. static defaultProps = {
  52. backgroundColor: '#394B58',
  53. term: null,
  54. links: [],
  55. hideColorOverlays: false,
  56. imagesEnabled: false,
  57. handleColorChange: () => {},
  58. image: '',
  59. reorderingEnabled: false,
  60. isDragging: false,
  61. connectDragSource: () => {},
  62. connectDropTarget: () => {},
  63. moveCard: () => {},
  64. totalCards: 0,
  65. position: 0
  66. }
  67. constructor (props) {
  68. super()
  69. this.state = _.extend(
  70. { nicknameInfo: this.nicknameInfo(props.shortName, props.originalName, props.id) },
  71. CourseActivitySummaryStore.getStateForCourse(props.id)
  72. )
  73. }
  74. // ===============
  75. // LIFECYCLE
  76. // ===============
  77. componentDidMount () {
  78. CourseActivitySummaryStore.addChangeListener(this.handleStoreChange)
  79. this.parentNode = this.cardDiv
  80. }
  81. componentWillUnmount () {
  82. CourseActivitySummaryStore.removeChangeListener(this.handleStoreChange)
  83. }
  84. // ===============
  85. // ACTIONS
  86. // ===============
  87. settingsClick = (e) => {
  88. if (e) { e.preventDefault(); }
  89. this.toggleEditing();
  90. }
  91. getCardPosition () {
  92. return typeof this.props.position === 'function' ? this.props.position() : this.props.position
  93. }
  94. handleNicknameChange = (nickname) => {
  95. this.setState({ nicknameInfo: this.nicknameInfo(nickname, this.props.originalName, this.props.id) })
  96. }
  97. handleStoreChange = () => {
  98. this.setState(
  99. CourseActivitySummaryStore.getStateForCourse(this.props.id)
  100. );
  101. }
  102. toggleEditing = () => {
  103. const currentState = !!this.state.editing;
  104. this.setState({editing: !currentState});
  105. }
  106. headerClick = (e) => {
  107. if (e) { e.preventDefault(); }
  108. window.location = this.props.href;
  109. }
  110. doneEditing = () => {
  111. this.setState({editing: false})
  112. this.settingsToggle.focus();
  113. }
  114. handleColorChange = (color) => {
  115. const hexColor = `#${color}`;
  116. this.props.handleColorChange(hexColor)
  117. }
  118. handleMove = (assetString, atIndex) => {
  119. if (typeof this.props.moveCard === 'function') {
  120. this.props.moveCard(assetString, atIndex, () => { this.settingsToggle.focus() })
  121. }
  122. }
  123. // ===============
  124. // HELPERS
  125. // ===============
  126. nicknameInfo (nickname, originalName, courseId) {
  127. return {
  128. nickname,
  129. originalName,
  130. courseId,
  131. onNicknameChange: this.handleNicknameChange
  132. }
  133. }
  134. unreadCount (icon, stream) {
  135. const activityType = {
  136. 'icon-announcement': 'Announcement',
  137. 'icon-assignment': 'Message',
  138. 'icon-discussion': 'DiscussionTopic'
  139. }[icon];
  140. const itemStream = stream || [];
  141. const streamItem = _.find(itemStream, item => (
  142. // only return 'Message' type if category is 'Due Date' (for assignments)
  143. item.type === activityType &&
  144. (activityType !== 'Message' || item.notification_category === I18n.t('Due Date'))
  145. ));
  146. // TODO: unread count is always 0 for assignments (see CNVS-21227)
  147. return (streamItem) ? streamItem.unread_count : 0;
  148. }
  149. calculateMenuOptions () {
  150. const position = this.getCardPosition()
  151. const isFirstCard = position === 0;
  152. const isLastCard = position === this.props.totalCards - 1;
  153. return {
  154. canMoveLeft: !isFirstCard,
  155. canMoveRight: !isLastCard,
  156. canMoveToBeginning: !isFirstCard,
  157. canMoveToEnd: !isLastCard
  158. }
  159. }
  160. // ===============
  161. // RENDERING
  162. // ===============
  163. linksForCard () {
  164. return this.props.links.map((link) => {
  165. if (!link.hidden) {
  166. const screenReaderLabel = `${link.label} - ${this.state.nicknameInfo.nickname}`;
  167. return (
  168. <DashboardCardAction
  169. unreadCount={this.unreadCount(link.icon, this.state.stream)}
  170. iconClass={link.icon}
  171. linkClass={link.css_class}
  172. path={link.path}
  173. screenReaderLabel={screenReaderLabel}
  174. key={link.path}
  175. />
  176. );
  177. }
  178. return null;
  179. });
  180. }
  181. renderHeaderHero () {
  182. const {
  183. imagesEnabled,
  184. image,
  185. backgroundColor,
  186. hideColorOverlays
  187. } = this.props;
  188. if (imagesEnabled && image) {
  189. return (
  190. <div
  191. className="ic-DashboardCard__header_image"
  192. style={{backgroundImage: `url(${image})`}}
  193. >
  194. <div
  195. className="ic-DashboardCard__header_hero"
  196. style={{backgroundColor, opacity: hideColorOverlays ? 0 : 0.6}}
  197. onClick={this.headerClick}
  198. aria-hidden="true"
  199. />
  200. </div>
  201. );
  202. }
  203. return (
  204. <div
  205. className="ic-DashboardCard__header_hero"
  206. style={{backgroundColor}}
  207. onClick={this.headerClick}
  208. aria-hidden="true"
  209. />
  210. );
  211. }
  212. renderHeaderButton () {
  213. const {
  214. backgroundColor,
  215. hideColorOverlays
  216. } = this.props;
  217. const reorderingProps = this.props.reorderingEnabled && {
  218. reorderingEnabled: this.props.reorderingEnabled,
  219. handleMove: this.handleMove,
  220. currentPosition: this.getCardPosition(),
  221. lastPosition: this.props.totalCards - 1,
  222. menuOptions: this.calculateMenuOptions()
  223. }
  224. const nickname = this.state.nicknameInfo.nickname
  225. return (
  226. <div>
  227. <div
  228. className="ic-DashboardCard__header-button-bg"
  229. style={{backgroundColor, opacity: hideColorOverlays ? 1 : 0}}
  230. />
  231. <DashboardCardMenu
  232. afterUpdateColor={this.handleColorChange}
  233. currentColor={this.props.backgroundColor}
  234. nicknameInfo={this.state.nicknameInfo}
  235. assetString={this.props.assetString}
  236. {...reorderingProps}
  237. trigger={
  238. <button
  239. className="Button Button--icon-action-rev ic-DashboardCard__header-button"
  240. ref={(c) => { this.settingsToggle = c }}
  241. >
  242. <i className="icon-more" aria-hidden="true" />
  243. <span className="screenreader-only">
  244. { this.props.reorderingEnabled
  245. ? I18n.t('Choose a color or course nickname or move course card for %{course}', { course: nickname })
  246. : I18n.t('Choose a color or course nickname for %{course}', { course: nickname })
  247. }
  248. </span>
  249. </button>
  250. }
  251. />
  252. </div>
  253. )
  254. }
  255. render () {
  256. const dashboardCard = (
  257. <div
  258. className="ic-DashboardCard"
  259. ref={(c) => { this.cardDiv = c }}
  260. style={{ opacity: (this.props.reorderingEnabled && this.props.isDragging) ? 0 : 1 }}
  261. aria-label={this.props.originalName}
  262. >
  263. <div className="ic-DashboardCard__header">
  264. <span className="screenreader-only">
  265. {
  266. this.props.imagesEnabled && this.props.image ?
  267. I18n.t('Course image for %{course}', {course: this.state.nicknameInfo.nickname})
  268. : I18n.t('Course card color region for %{course}', {course: this.state.nicknameInfo.nickname})
  269. }
  270. </span>
  271. {this.renderHeaderHero()}
  272. <a href={this.props.href} className="ic-DashboardCard__link">
  273. <div className="ic-DashboardCard__header_content">
  274. <h2 className="ic-DashboardCard__header-title ellipsis" title={this.props.originalName}>
  275. <span style={{color: this.props.backgroundColor}}>
  276. {this.state.nicknameInfo.nickname}
  277. </span>
  278. </h2>
  279. <div
  280. className="ic-DashboardCard__header-subtitle ellipsis"
  281. title={this.props.courseCode}
  282. >
  283. {this.props.courseCode}
  284. </div>
  285. <div
  286. className="ic-DashboardCard__header-term ellipsis"
  287. title={this.props.term}
  288. >
  289. {(this.props.term) ? this.props.term : null}
  290. </div>
  291. </div>
  292. </a>
  293. { this.renderHeaderButton() }
  294. </div>
  295. <nav
  296. className="ic-DashboardCard__action-container"
  297. aria-label={I18n.t('Actions for %{course}', {course: this.state.nicknameInfo.nickname})}
  298. >
  299. { this.linksForCard() }
  300. </nav>
  301. </div>
  302. );
  303. if (this.props.reorderingEnabled) {
  304. const { connectDragSource, connectDropTarget } = this.props;
  305. return connectDragSource(connectDropTarget(dashboardCard));
  306. }
  307. return dashboardCard;
  308. }
  309. }