polls: Add option for modal to create polls.

Earlier the `/poll` slash command was the only way to create polls.
To increase user friendliness with a GUI, a button to launch a modal
to create a poll, has been added to the compose box. This button is
enabled only when the compose box is empty, to avoid complexities with
losing / having to save as draft any message already being composed.

The modal has a form which on submission frames a message using the
`/poll` syntax and the data input in the form, and sets the content of
the compose box to that message, which the user can then send. The
question field is mandatory for form submission.

Fixes: #20304.
This commit is contained in:
N-Shar-ma 2022-07-16 18:10:59 +05:30 committed by Tim Abbott
parent 084718b776
commit 143db56992
12 changed files with 255 additions and 0 deletions

View File

@ -163,6 +163,7 @@ EXEMPT_FILES = make_set(
"web/src/plotly.js.d.ts",
"web/src/pm_list.js",
"web/src/pm_list_dom.ts",
"web/src/poll_modal.js",
"web/src/poll_widget.ts",
"web/src/popover_menus.js",
"web/src/popover_menus_data.js",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -117,6 +117,7 @@ export function clear_compose_box() {
compose_banner.clear_uploads();
compose_ui.hide_compose_spinner();
scheduled_messages.reset_selected_schedule_timestamp();
$(".compose_control_button_container:has(.add-poll)").removeClass("disabled-on-hover");
}
export function send_message_success(request, data) {

View File

@ -1,5 +1,7 @@
import $ from "jquery";
import render_add_poll_modal from "../templates/add_poll_modal.hbs";
import * as compose from "./compose";
import * as compose_actions from "./compose_actions";
import * as compose_banner from "./compose_banner";
@ -9,10 +11,13 @@ import * as compose_recipient from "./compose_recipient";
import * as compose_state from "./compose_state";
import * as compose_ui from "./compose_ui";
import * as compose_validate from "./compose_validate";
import * as dialog_widget from "./dialog_widget";
import * as flatpickr from "./flatpickr";
import {$t_html} from "./i18n";
import * as message_edit from "./message_edit";
import * as narrow from "./narrow";
import {page_params} from "./page_params";
import * as poll_modal from "./poll_modal";
import * as popovers from "./popovers";
import * as resize from "./resize";
import * as rows from "./rows";
@ -23,6 +28,7 @@ import * as stream_settings_components from "./stream_settings_components";
import * as sub_store from "./sub_store";
import * as subscriber_api from "./subscriber_api";
import {get_timestamp_for_flatpickr} from "./timerender";
import * as ui_report from "./ui_report";
import * as upload from "./upload";
import * as user_topics from "./user_topics";
@ -74,6 +80,13 @@ export function initialize() {
} else {
$("#compose_close").attr("data-tooltip-template-id", "compose_close_tooltip_template");
}
// The poll widget requires an empty compose box.
if (compose_text_length > 0) {
$(".add-poll").parent().addClass("disabled-on-hover");
} else {
$(".add-poll").parent().removeClass("disabled-on-hover");
}
});
$("#compose form").on("submit", (e) => {
@ -325,6 +338,44 @@ export function initialize() {
}
});
$("body").on("click", ".compose_control_button_container:not(.disabled) .add-poll", (e) => {
e.preventDefault();
e.stopPropagation();
function validate_input() {
const question = $("#poll-question-input").val().trim();
if (question === "") {
ui_report.error(
$t_html({defaultMessage: "Please enter a question."}),
undefined,
$("#dialog_error"),
);
return false;
}
return true;
}
dialog_widget.launch({
html_heading: $t_html({defaultMessage: "Create a poll"}),
html_body: render_add_poll_modal(),
html_submit_button: $t_html({defaultMessage: "Add poll"}),
close_on_submit: true,
on_click(e) {
// frame a message using data input in modal, then populate the compose textarea with it
e.preventDefault();
e.stopPropagation();
const poll_message_content = poll_modal.frame_poll_message_content();
compose_ui.insert_syntax_and_focus(poll_message_content);
},
validate_input,
form_id: "add-poll-form",
id: "add-poll-modal",
post_render: poll_modal.poll_options_setup,
help_link: "https://zulip.com/help/create-a-poll",
});
});
$("#compose").on("click", ".markdown_preview", (e) => {
e.preventDefault();
e.stopPropagation();

68
web/src/poll_modal.js Normal file
View File

@ -0,0 +1,68 @@
import $ from "jquery";
import {Sortable} from "sortablejs";
import render_poll_modal_option from "../templates/poll_modal_option.hbs";
function create_option_row($last_option_row_input) {
const row = render_poll_modal_option();
const $row_container = $last_option_row_input.closest(".simplebar-content");
$row_container.append(row);
}
function add_option_row(e) {
// if the option triggering the input event e is not the last,
// that is, its next sibling has the class `option-row`, we
// do not add a new option row and return from this function
// This handles a case when the next empty input row is already
// added and user is updating the above row(s).
if ($(e.target).closest(".option-row").next().hasClass("option-row")) {
return;
}
create_option_row($(e.target));
}
function delete_option_row(e) {
const $row = $(e.target).closest(".option-row");
$row.remove();
}
export function poll_options_setup() {
const $poll_options_list = $("#add-poll-form .poll-options-list");
const $submit_button = $("#add-poll-modal .dialog_submit_button");
const $question_input = $("#add-poll-form #poll-question-input");
// Disable the submit button if the question is empty.
$submit_button.prop("disabled", true);
$question_input.on("input", () => {
if ($question_input.val().trim() !== "") {
$submit_button.prop("disabled", false);
} else {
$submit_button.prop("disabled", true);
}
});
$poll_options_list.on("input", "input.poll-option-input", add_option_row);
$poll_options_list.on("click", "button.delete-option", delete_option_row);
// setTimeout is needed to here to give time for simplebar to initialise
setTimeout(() => {
Sortable.create($("#add-poll-form .poll-options-list .simplebar-content")[0], {
onUpdate() {},
// We don't want the last (empty) row to be draggable, as a new row
// is added on input event of the last row.
filter: "input, .option-row:last-child",
preventOnFilter: false,
});
}, 0);
}
export function frame_poll_message_content() {
const question = $("#poll-question-input").val().trim();
const options = $(".poll-option-input")
.map(function () {
return $(this).val().trim();
})
.toArray()
.filter(Boolean);
return "/poll " + question + "\n" + options.join("\n");
}

View File

@ -209,6 +209,22 @@ export function initialize(): void {
},
});
delegate("body", {
target: "#add-poll-modal .dialog_submit_button_container",
appendTo: () => document.body,
onShow(instance) {
const content = $t({defaultMessage: "Please enter a question."});
const $elem = $(instance.reference);
// Show tooltip to enter question only if submit button is disabled
// (due to question field being empty).
if ($elem.find(".dialog_submit_button").is(":disabled")) {
instance.setContent(content);
return undefined;
}
return false;
},
});
$("body").on(
"blur",
".message_control_button, .delete-selected-drafts-button-container",

View File

@ -994,6 +994,15 @@ textarea.new_message_textarea,
}
}
.compose_control_button_container.disabled-on-hover:hover {
opacity: 0.3;
cursor: not-allowed;
.compose_control_button {
pointer-events: none;
}
}
.fa-eye {
position: relative;
top: -0.7px;

View File

@ -385,3 +385,81 @@
0 0 8px hsl(206deg 80% 62% / 60%);
}
}
#add-poll-modal {
/* this height allows 3-4 option rows
to fit in without need for scrolling */
height: 450px;
overflow: hidden;
.modal__content {
flex-grow: 1;
.simplebar-content {
box-sizing: border-box;
height: 100%;
}
}
#add-poll-form {
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
.poll-label {
font-weight: bold;
margin: 5px 0;
}
.poll-question-input-container {
display: flex;
margin-bottom: 10px;
#poll-question-input {
flex-grow: 1;
}
}
.poll-options-list {
margin: 0;
height: 0;
overflow: auto;
flex-grow: 1;
.option-row {
list-style-type: none;
cursor: move;
margin-top: 10px;
padding: 0;
display: flex;
align-items: center;
gap: 10px;
.drag-icon {
color: hsl(0deg 0% 75%);
}
.poll-option-input {
flex-grow: 1;
}
}
.option-row:first-child {
margin-top: 0;
}
.option-row:last-child {
cursor: default;
.delete-option {
visibility: hidden;
}
.drag-icon {
visibility: hidden;
}
}
}
}
}

View File

@ -0,0 +1,13 @@
<form id="add-poll-form" class="new-style">
<label class="poll-label">{{t "Question"}}</label>
<div class="poll-question-input-container">
<input type="text" id="poll-question-input" class="modal_text_input" placeholder="{{t 'Your question'}}" />
</div>
<label class="poll-label">{{t "Options"}}</label>
<p>{{t "Anyone can add more options after the poll is posted."}}</p>
<ul class="poll-options-list" data-simplebar>
{{> poll_modal_option }}
{{> poll_modal_option }}
{{> poll_modal_option }}
</ul>
</form>

View File

@ -24,6 +24,11 @@
<div class="compose_control_button_container preview_mode_disabled" data-tooltip-template-id="add-global-time-tooltip" data-tippy-maxWidth="none">
<a role="button" class="compose_control_button fa fa-clock-o time_pick" aria-label="{{t 'Add global time' }}" tabindex=0></a>
</div>
{{#unless message_id}}
<div class="compose_control_button_container preview_mode_disabled" data-tooltip-template-id="add-poll-tooltip" data-tippy-maxWidth="none">
<a role="button" class="compose_control_button fa fa-hand-paper-o add-poll" aria-label="{{t 'Add poll' }}" tabindex=0></a>
</div>
{{/unless}}
<div class="compose_control_button_container {{#unless giphy_enabled }}hide{{/unless}} preview_mode_disabled" data-tippy-content="{{t 'Add GIF' }}">
<a role="button" class="compose_control_button compose_gif_icon zulip-icon zulip-icon-gif" aria-label="{{t 'Add GIF' }}" tabindex=0></a>
</div>

View File

@ -0,0 +1,7 @@
<li class="option-row">
<i class="zulip-icon zulip-icon-grip-vertical drag-icon"></i>
<input type="text" class="poll-option-input modal_text_input" placeholder="{{t 'New option' }}" />
<button type="button" class="button rounded small btn-secondary delete-option" title="{{t 'Delete' }}">
<i class="fa fa-trash-o" aria-hidden="true"></i>
</button>
</li>

View File

@ -59,6 +59,12 @@
<div class="tooltip-inner-content italic">{{t "Everyone sees global times in their own time zone." }}</div>
</div>
</template>
<template id="add-poll-tooltip">
<div>
<span>{{t "Add poll" }}</span><br/>
<span class="tooltip-inner-content italic">{{t "A poll must be an entire message." }}</span>
</div>
</template>
<template id="delete-draft-tooltip-template">
{{t 'Delete draft' }}
{{tooltip_hotkey_hints "Backspace"}}