canvas-lms/ui/shared/publish-button-view/backbone/views/index.jsx

452 lines
13 KiB
JavaScript

/*
* Copyright (C) 2023 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-void */
import {extend} from '@canvas/backbone/utils'
import {useScope as useI18nScope} from '@canvas/i18n'
import $ from 'jquery'
import Backbone from '@canvas/backbone'
import htmlEscape from '@instructure/html-escape'
import '@canvas/jquery/jquery.instructure_forms'
import * as tz from '@instructure/moment-utils'
import React from 'react'
import ReactDOM from 'react-dom'
import DelayedPublishDialog from '../../react/components/DelayedPublishDialog'
const I18n = useI18nScope('publish_btn_module')
export default (function (superClass) {
extend(PublishButton, superClass)
function PublishButton() {
this.renderDelayedPublish = this.renderDelayedPublish.bind(this)
return PublishButton.__super__.constructor.apply(this, arguments)
}
PublishButton.prototype.disabledClass = 'disabled'
PublishButton.prototype.publishClass = 'btn-publish'
PublishButton.prototype.publishedClass = 'btn-published'
PublishButton.prototype.unpublishClass = 'btn-unpublish'
// This value allows the text to include the item title
PublishButton.optionProperty('title')
// These values allow the default text to be overridden if necessary
PublishButton.optionProperty('publishText')
PublishButton.optionProperty('unpublishText')
// # This indicates that the button is disabled specifically because it is
// # associated with a moderated assignment that the current user does not
// # have the Select Final Grade permission.
PublishButton.optionProperty('disabledForModeration')
PublishButton.prototype.tagName = 'button'
PublishButton.prototype.className = 'btn'
PublishButton.prototype.events = {
click: 'click',
mouseenter: 'handleMouseEnter',
mouseleave: 'handleMouseLeave',
}
PublishButton.prototype.els = {
i: '$icon',
'.publish-text': '$text',
'.dpd-mount': '$dpd_mount',
}
PublishButton.prototype.initialize = function () {
let ref
PublishButton.__super__.initialize.apply(this, arguments)
return (ref = this.model) != null
? ref.on(
'change:unpublishable',
(function (_this) {
return function () {
if (!_this.model.get('unpublishable') && _this.model.get('published')) {
return _this.disable()
}
}
})(this)
)
: void 0
}
PublishButton.prototype.setElement = function () {
PublishButton.__super__.setElement.apply(this, arguments)
if (!this.model.get('unpublishable') && this.model.get('published')) {
return this.disable()
}
}
// events
PublishButton.prototype.handleMouseEnter = function () {
if (this.isDelayedPublish()) {
return
}
if (this.keepState || this.isPublish() || this.isDisabled()) {
return
}
this.renderUnpublish()
this.keepState = true
}
PublishButton.prototype.handleMouseLeave = function () {
if (this.isDelayedPublish()) {
return
}
this.keepState = false
if (!(this.isPublish() || this.isDisabled())) {
this.renderPublished()
}
}
PublishButton.prototype.click = function (event) {
if (this.isDelayedPublish()) {
return this.openDelayedPublishDialog()
}
event.preventDefault()
event.stopPropagation()
if (this.isDisabled()) {
return
}
this.keepState = true
if (this.isPublish()) {
return this.publish()
} else if (this.isUnpublish() || this.isPublished()) {
return this.unpublish()
}
}
PublishButton.prototype.addAriaLabel = function (label) {
let $label = this.$el.find('span.screenreader-only.accessible_label')
if (!$label.length) {
$label = $('<span class="screenreader-only accessible_label"></span>').appendTo(this.$el)
}
$label.text(label)
return this.$el.attr('aria-label', label)
}
PublishButton.prototype.setFocusToElement = function () {
return this.$el.focus()
}
// calling publish/unpublish on the model expects a deferred object
PublishButton.prototype.publish = function (_event) {
this.renderPublishing()
return this.model.publish().always(
(function (_this) {
return function () {
let ref, ref1
_this.trigger('publish')
_this.enable()
_this.render()
_this.setFocusToElement()
if (
!['discussion_topic', 'quiz', 'assignment'].includes(
_this.model.attributes.module_type
) ||
(_this.model.attributes.module_type === 'discussion_topic' &&
!((ref = _this.$el[0]) != null
? (ref1 = ref.dataset) != null
? ref1.assignmentId
: void 0
: void 0))
) {
return false
}
const $sgLink = $(
'#speed-grader-container-' +
_this.model.attributes.module_type +
'-' +
_this.model.attributes.content_id
)
return $sgLink.removeClass('hidden')
}
})(this)
)
}
PublishButton.prototype.unpublish = function (_event) {
this.renderUnpublishing()
return this.model
.unpublish()
.done(
(function (_this) {
return function () {
_this.trigger('unpublish')
_this.disable()
_this.render()
_this.setFocusToElement()
const $sgLink = $(
'#speed-grader-container-' +
_this.model.attributes.module_type +
'-' +
_this.model.attributes.content_id
)
return $sgLink.addClass('hidden')
}
})(this)
)
.fail(
(function (_this) {
return function (error) {
if (error.status === 403) {
$.flashError(_this.model.disabledMessage())
}
_this.disable()
_this.renderPublished()
return _this.setFocusToElement()
}
})(this)
)
}
// state
PublishButton.prototype.isPublish = function () {
return this.$el.hasClass(this.publishClass)
}
PublishButton.prototype.isPublished = function () {
return this.$el.hasClass(this.publishedClass)
}
PublishButton.prototype.isUnpublish = function () {
return this.$el.hasClass(this.unpublishClass)
}
PublishButton.prototype.isDisabled = function () {
return this.$el.hasClass(this.disabledClass)
}
PublishButton.prototype.isDelayedPublish = function () {
let ref
return (
(typeof ENV !== 'undefined' && ENV !== null
? (ref = ENV.FEATURES) != null
? ref.scheduled_page_publication
: void 0
: void 0) &&
!this.model.get('published') &&
this.model.get('publish_at')
)
}
PublishButton.prototype.disable = function () {
return this.$el.addClass(this.disabledClass)
}
PublishButton.prototype.enable = function () {
return this.$el.removeClass(this.disabledClass)
}
PublishButton.prototype.reset = function () {
this.$el.removeClass(
this.publishClass +
' ' +
this.publishedClass +
' ' +
this.unpublishClass +
' published-status restricted'
)
this.$icon.removeClass('icon-publish icon-unpublish icon-unpublished')
return this.$el.removeAttr('title aria-label')
}
PublishButton.prototype.publishLabel = function () {
if (this.publishText) {
return this.publishText
}
if (this.title) {
return I18n.t('Unpublished. Click to publish %{title}.', {
title: this.title,
})
}
return I18n.t('Unpublished. Click to publish.')
}
PublishButton.prototype.unpublishLabel = function () {
if (this.unpublishText) {
return this.unpublishText
}
if (this.title) {
return I18n.t('Published. Click to unpublish %{title}.', {
title: this.title,
})
}
return I18n.t('Published. Click to unpublish.')
}
// render
PublishButton.prototype.render = function () {
if (!this.$el.is('button')) {
this.$el.attr('role', 'button')
}
this.$el.attr('tabindex', '0')
this.$el.html('<i></i><span class="publish-text"></span><span class="dpd-mount"></span>')
this.cacheEls()
// don't read text of button with screenreader
this.$text.attr('tabindex', '-1')
if (this.model.get('published')) {
this.renderPublished()
} else if (this.isDelayedPublish()) {
this.renderDelayedPublish()
} else {
this.renderPublish()
}
if (this.model.get('bulkPublishInFlight')) {
this.disable()
}
return this
}
PublishButton.prototype.renderPublish = function () {
return this.renderState({
text: I18n.t('buttons.publish', 'Publish'),
label: this.publishLabel(),
buttonClass: this.publishClass,
iconClass: 'icon-unpublish',
})
}
PublishButton.prototype.renderPublished = function () {
return this.renderState({
text: I18n.t('buttons.published', 'Published'),
label: this.unpublishLabel(),
buttonClass: this.publishedClass,
iconClass: 'icon-publish icon-Solid',
})
}
PublishButton.prototype.renderUnpublish = function () {
const text = I18n.t('buttons.unpublish', 'Unpublish')
return this.renderState({
text,
buttonClass: this.unpublishClass,
iconClass: 'icon-unpublish',
})
}
PublishButton.prototype.renderPublishing = function () {
this.disable()
const text = I18n.t('buttons.publishing', 'Publishing...')
return this.renderState({
text,
buttonClass: this.publishClass,
iconClass: 'icon-publish icon-Solid',
})
}
PublishButton.prototype.renderUnpublishing = function () {
this.disable()
const text = I18n.t('buttons.unpublishing', 'Unpublishing...')
return this.renderState({
text,
buttonClass: this.unpublishClass,
iconClass: 'icon-unpublished',
})
}
PublishButton.prototype.renderDelayedPublish = function () {
return this.renderState({
text: I18n.t('Will publish on %{publish_date}', {
publish_date: tz.format(this.model.get('publish_at'), 'date.formats.short'),
}),
iconClass: 'icon-calendar-month',
buttonClass: this.$el.is('button') ? '' : 'published-status restricted',
})
}
PublishButton.prototype.renderState = function (options) {
this.reset()
this.$el.addClass(options.buttonClass)
this.$el.attr('aria-pressed', options.buttonClass === this.publishedClass)
this.$icon.addClass(options.iconClass)
this.$text.html('&nbsp;' + htmlEscape(options.text))
// a riff on the code from initPublishButton
const $row = this.$el.closest('.ig-row')
$row.toggleClass('ig-published', this.model.get('published'))
// uneditable because the current user does not have the Select Final
// Grade permission.
if (this.model.get('disabledForModeration')) {
return this.disableWithMessage(
'You do not have permissions to edit this moderated assignment'
)
// unpublishable (i.e., able to be unpublished)
} else if (this.model.get('unpublishable') == null || this.model.get('unpublishable')) {
this.enable()
this.$el.data('tooltip', 'left')
this.$el.attr('title', options.text)
if (options.label) {
return this.addAriaLabel(options.label)
}
// editable, but cannot be unpublished because submissions exist
} else if (this.model.get('published')) {
return this.disableWithMessage(this.model.disabledMessage())
}
}
PublishButton.prototype.disableWithMessage = function (message) {
this.disable()
this.$el.attr('aria-disabled', true)
this.$el.attr('title', message)
this.$el.data('tooltip', 'left')
return this.addAriaLabel(message)
}
PublishButton.prototype.openDelayedPublishDialog = function () {
const props = {
name: this.model.get('title') || this.model.get('module_item_name'),
courseId: ENV.COURSE_ID,
contentId: this.model.get('page_url') || this.model.get('url') || this.model.get('id'),
publishAt: this.model.get('publish_at'),
onPublish: (function (_this) {
return function () {
return _this.publish()
}
})(this),
onUpdatePublishAt: (function (_this) {
return function (val) {
_this.model.set('publish_at', val)
_this.render()
return _this.setFocusToElement()
}
})(this),
onClose: (function (_this) {
return function () {
return ReactDOM.unmountComponentAtNode(_this.$dpd_mount[0])
}
})(this),
}
// eslint-disable-next-line react/no-render-return-value
return ReactDOM.render(React.createElement(DelayedPublishDialog, props), this.$dpd_mount[0])
}
return PublishButton
})(Backbone.View)