From 0c89d8c086b4d6630d56d0ecc822184ca0683076 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 7 Apr 2024 13:42:39 +0100 Subject: [PATCH] feat: `safe_mode_extensions` --- framework/core/js/dist/admin.js.LICENSE.txt | 63 -- .../dist/common/components/EditUserModal.js | 201 +++++- .../common/components/EditUserModal.js.map | 2 +- framework/core/js/dist/forum.js.LICENSE.txt | 63 -- .../core/js/dist/forum/components/Composer.js | 418 ++++++++++++- .../js/dist/forum/components/Composer.js.map | 2 +- .../forum/components/DiscussionComposer.js | 288 ++++++++- .../components/DiscussionComposer.js.map | 2 +- .../forum/components/DiscussionsUserPage.js | 52 +- .../components/DiscussionsUserPage.js.map | 2 +- .../dist/forum/components/EditPostComposer.js | 293 ++++++++- .../forum/components/EditPostComposer.js.map | 2 +- .../forum/components/ForgotPasswordModal.js | 129 +++- .../components/ForgotPasswordModal.js.map | 2 +- .../js/dist/forum/components/LogInModal.js | 222 ++++++- .../dist/forum/components/LogInModal.js.map | 2 +- .../forum/components/NotificationsPage.js | 47 +- .../forum/components/NotificationsPage.js.map | 2 +- .../js/dist/forum/components/PostStream.js | 590 +++++++++++++++++- .../dist/forum/components/PostStream.js.map | 2 +- .../forum/components/PostStreamScrubber.js | 339 +++++++++- .../components/PostStreamScrubber.js.map | 2 +- .../js/dist/forum/components/ReplyComposer.js | 296 ++++++++- .../forum/components/ReplyComposer.js.map | 2 +- .../js/dist/forum/components/SearchModal.js | 386 +++++++++++- .../dist/forum/components/SearchModal.js.map | 2 +- .../js/dist/forum/components/SettingsPage.js | 551 +++++++++++++++- .../dist/forum/components/SettingsPage.js.map | 2 +- .../js/dist/forum/components/SignUpModal.js | 223 ++++++- .../dist/forum/components/SignUpModal.js.map | 2 +- .../dist/forum/components/UserSecurityPage.js | 561 ++++++++++++++++- .../forum/components/UserSecurityPage.js.map | 2 +- .../core/js/src/admin/AdminApplication.tsx | 1 + .../js/src/admin/components/AdminPage.tsx | 44 +- .../js/src/admin/components/AdvancedPage.tsx | 25 + .../js/src/admin/components/ExtensionPage.tsx | 2 +- framework/core/js/src/common/Application.tsx | 36 +- .../js/src/common/components/Dropdown.tsx | 3 + .../js/src/common/components/FormGroup.tsx | 33 +- .../js/src/common/components/MultiSelect.tsx | 113 ++++ .../core/less/admin/FormSectionGroup.less | 4 + framework/core/less/common/Button.less | 6 + framework/core/less/common/Form.less | 2 +- framework/core/less/common/Select.less | 4 + framework/core/locale/core.yml | 8 +- .../core/src/Admin/AdminServiceProvider.php | 27 +- .../core/src/Admin/Content/AdminPayload.php | 1 + .../core/src/Admin/WhenSavingSettings.php | 42 ++ framework/core/src/Extend/Settings.php | 21 + .../core/src/Extension/ExtensionManager.php | 16 +- .../Extension/ExtensionServiceProvider.php | 11 +- .../core/src/Forum/ForumServiceProvider.php | 5 + framework/core/src/Foundation/Application.php | 1 + framework/core/src/Foundation/Config.php | 5 + .../core/src/Foundation/MaintenanceMode.php | 12 + framework/core/src/Frontend/AssetManager.php | 56 ++ .../src/Frontend/FrontendServiceProvider.php | 8 + 57 files changed, 5031 insertions(+), 207 deletions(-) delete mode 100644 framework/core/js/dist/admin.js.LICENSE.txt delete mode 100644 framework/core/js/dist/forum.js.LICENSE.txt create mode 100644 framework/core/js/src/common/components/MultiSelect.tsx create mode 100644 framework/core/src/Admin/WhenSavingSettings.php create mode 100644 framework/core/src/Frontend/AssetManager.php diff --git a/framework/core/js/dist/admin.js.LICENSE.txt b/framework/core/js/dist/admin.js.LICENSE.txt deleted file mode 100644 index da067cd7bba..00000000000 --- a/framework/core/js/dist/admin.js.LICENSE.txt +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * quantize.js Copyright 2008 Nick Rabinowitz. - * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php - */ - -/*! - * Sizzle CSS Selector Engine v2.3.6 - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://js.foundation/ - * - * Date: 2021-02-16 - */ - -/*! - * Block below copied from Protovis: http://mbostock.github.com/protovis/ - * Copyright 2010 Stanford Visualization Group - * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php - */ - -/*! - * Color Thief v2.0 - * by Lokesh Dhakar - http://www.lokeshdhakar.com - * - * Thanks - * ------ - * Nick Rabinowitz - For creating quantize.js. - * John Schulz - For clean up and optimization. @JFSIII - * Nathan Spady - For adding drag and drop support to the demo page. - * - * License - * ------- - * Copyright 2011, 2015 Lokesh Dhakar - * Released under the MIT license - * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE - * - */ - -/*! - * jQuery JavaScript Library v3.6.1 - * https://jquery.com/ - * - * Includes Sizzle.js - * https://sizzlejs.com/ - * - * Copyright OpenJS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2022-08-26T17:52Z - */ - -/*! -* focus-trap 6.9.4 -* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE -*/ - -/*! -* tabbable 5.3.3 -* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE -*/ diff --git a/framework/core/js/dist/common/components/EditUserModal.js b/framework/core/js/dist/common/components/EditUserModal.js index abf6da6e057..c042e90b8dc 100644 --- a/framework/core/js/dist/common/components/EditUserModal.js +++ b/framework/core/js/dist/common/components/EditUserModal.js @@ -1,2 +1,201 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[841],{4292:(s,t,i)=>{i.r(t),i.d(t,{default:()=>p});var e=i(7905),r=i(7465),a=i(899),n=i(8312),o=i(6697),d=i(7645),l=i(1552),u=i(4041),h=i(6458),c=i(6352);class p extends a.Z{constructor(){super(...arguments),(0,e.Z)(this,"username",void 0),(0,e.Z)(this,"email",void 0),(0,e.Z)(this,"isEmailConfirmed",void 0),(0,e.Z)(this,"setPassword",void 0),(0,e.Z)(this,"password",void 0),(0,e.Z)(this,"groups",{})}oninit(s){super.oninit(s);const t=this.attrs.user;this.username=(0,h.Z)(t.username()||""),this.email=(0,h.Z)(t.email()||""),this.isEmailConfirmed=(0,h.Z)(t.isEmailConfirmed()||!1),this.setPassword=(0,h.Z)(!1),this.password=(0,h.Z)(t.password()||"");const i=t.groups()||[];r.Z.store.all("groups").filter((s=>![d.Z.GUEST_ID,d.Z.MEMBER_ID].includes(s.id()))).forEach((s=>this.groups[s.id()]=(0,h.Z)(i.includes(s))))}className(){return"EditUserModal Modal--small"}title(){return r.Z.translator.trans("core.lib.edit_user.title")}content(){const s=this.fields().toArray();return m("div",{className:"Modal-body"},s.length>1?m(c.Z,null,this.fields().toArray()):r.Z.translator.trans("core.lib.edit_user.nothing_available"))}fields(){const s=new u.Z;return this.attrs.user.canEditCredentials()&&(s.add("username",m("div",{className:"Form-group"},m("label",null,r.Z.translator.trans("core.lib.edit_user.username_heading")),m("input",{className:"FormControl",placeholder:(0,l.Z)(r.Z.translator.trans("core.lib.edit_user.username_label")),bidi:this.username,disabled:this.nonAdminEditingAdmin()})),40),r.Z.session.user!==this.attrs.user&&(s.add("email",m("div",{className:"Form-group"},m("label",null,r.Z.translator.trans("core.lib.edit_user.email_heading")),m("input",{className:"FormControl",placeholder:(0,l.Z)(r.Z.translator.trans("core.lib.edit_user.email_label")),bidi:this.email,disabled:this.nonAdminEditingAdmin()}),!this.isEmailConfirmed()&&this.userIsAdmin(r.Z.session.user)&&m(n.Z,{className:"Button Button--block",loading:this.loading,onclick:this.activate.bind(this)},r.Z.translator.trans("core.lib.edit_user.activate_button"))),30),s.add("password",m("div",{className:"Form-group"},m("label",null,r.Z.translator.trans("core.lib.edit_user.password_heading")),m("div",null,m("label",{className:"checkbox"},m("input",{type:"checkbox",onchange:s=>{const t=s.target;this.setPassword(t.checked),m.redraw.sync(),t.checked&&this.$("[name=password]").select(),s.redraw=!1},disabled:this.nonAdminEditingAdmin()}),r.Z.translator.trans("core.lib.edit_user.set_password_label"))),this.setPassword()&&m("input",{className:"FormControl",type:"password",name:"password",placeholder:(0,l.Z)(r.Z.translator.trans("core.lib.edit_user.password_label")),bidi:this.password,disabled:this.nonAdminEditingAdmin()})),20))),this.attrs.user.canEditGroups()&&s.add("groups",m("div",{className:"Form-group EditUserModal-groups"},m("label",null,r.Z.translator.trans("core.lib.edit_user.groups_heading")),m("div",null,Object.keys(this.groups).map((s=>r.Z.store.getById("groups",s))).filter(Boolean).map((s=>s&&m("label",{className:"checkbox"},m("input",{type:"checkbox",bidi:this.groups[s.id()],disabled:s.id()===d.Z.ADMINISTRATOR_ID&&(this.attrs.user===r.Z.session.user||!this.userIsAdmin(r.Z.session.user))}),m(o.Z,{group:s,label:null})," ",s.nameSingular()))))),10),s.add("submit",m("div",{className:"Form-group Form-controls"},m(n.Z,{className:"Button Button--primary",type:"submit",loading:this.loading},r.Z.translator.trans("core.lib.edit_user.submit_button"))),-10),s}activate(){this.loading=!0;const s={username:this.username(),isEmailConfirmed:!0};this.attrs.user.save(s,{errorHandler:this.onerror.bind(this)}).then((()=>{this.isEmailConfirmed(!0),this.loading=!1,m.redraw()})).catch((()=>{this.loading=!1,m.redraw()}))}data(){const s={},t={};return this.attrs.user.canEditCredentials()&&!this.nonAdminEditingAdmin()&&(s.username=this.username(),r.Z.session.user!==this.attrs.user&&(s.email=this.email()),this.setPassword()&&(s.password=this.password())),this.attrs.user.canEditGroups()&&(t.groups=Object.keys(this.groups).filter((s=>this.groups[s]())).map((s=>r.Z.store.getById("groups",s))).filter((s=>s instanceof d.Z))),s.relationships=t,s}onsubmit(s){s.preventDefault(),this.loading=!0,this.attrs.user.save(this.data(),{errorHandler:this.onerror.bind(this)}).then(this.hide.bind(this)).catch((()=>{this.loading=!1,m.redraw()}))}nonAdminEditingAdmin(){return this.userIsAdmin(this.attrs.user)&&!this.userIsAdmin(r.Z.session.user)}userIsAdmin(s){return!!((null==s?void 0:s.groups())||[]).some((s=>(null==s?void 0:s.id())===d.Z.ADMINISTRATOR_ID))}}flarum.reg.add("core","common/components/EditUserModal",p)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["common/components/EditUserModal"],{ + +/***/ "./src/common/components/EditUserModal.tsx": +/*!*************************************************!*\ + !*** ./src/common/components/EditUserModal.tsx ***! + \*************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ EditUserModal) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _common_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/app */ "./src/common/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _GroupBadge__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./GroupBadge */ "./src/common/components/GroupBadge.tsx"); +/* harmony import */ var _models_Group__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../models/Group */ "./src/common/models/Group.ts"); +/* harmony import */ var _utils_extractText__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _utils_ItemList__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _utils_Stream__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../utils/Stream */ "./src/common/utils/Stream.ts"); +/* harmony import */ var _Form__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./Form */ "./src/common/components/Form.tsx"); + + + + + + + + + + +class EditUserModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "username", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "email", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "isEmailConfirmed", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "setPassword", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "password", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "groups", {}); + } + oninit(vnode) { + super.oninit(vnode); + const user = this.attrs.user; + this.username = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.username() || ''); + this.email = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.email() || ''); + this.isEmailConfirmed = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.isEmailConfirmed() || false); + this.setPassword = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(false); + this.password = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(user.password() || ''); + const userGroups = user.groups() || []; + _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].store.all('groups').filter(group => ![_models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].GUEST_ID, _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].MEMBER_ID].includes(group.id())).forEach(group => this.groups[group.id()] = (0,_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(userGroups.includes(group))); + } + className() { + return 'EditUserModal Modal--small'; + } + title() { + return _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.title'); + } + content() { + const fields = this.fields().toArray(); + return m("div", { + className: "Modal-body" + }, fields.length > 1 ? m(_Form__WEBPACK_IMPORTED_MODULE_9__["default"], null, this.fields().toArray()) : _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.nothing_available')); + } + fields() { + const items = new _utils_ItemList__WEBPACK_IMPORTED_MODULE_7__["default"](); + if (this.attrs.user.canEditCredentials()) { + items.add('username', m("div", { + className: "Form-group" + }, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.username_heading')), m("input", { + className: "FormControl", + placeholder: (0,_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.username_label')), + bidi: this.username, + disabled: this.nonAdminEditingAdmin() + })), 40); + if (_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user !== this.attrs.user) { + items.add('email', m("div", { + className: "Form-group" + }, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.email_heading')), m("input", { + className: "FormControl", + placeholder: (0,_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.email_label')), + bidi: this.email, + disabled: this.nonAdminEditingAdmin() + }), !this.isEmailConfirmed() && this.userIsAdmin(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user) && m(_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--block", + loading: this.loading, + onclick: this.activate.bind(this) + }, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.activate_button'))), 30); + items.add('password', m("div", { + className: "Form-group" + }, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.password_heading')), m("div", null, m("label", { + className: "checkbox" + }, m("input", { + type: "checkbox", + onchange: e => { + const target = e.target; + this.setPassword(target.checked); + m.redraw.sync(); + if (target.checked) this.$('[name=password]').select(); + e.redraw = false; + }, + disabled: this.nonAdminEditingAdmin() + }), _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.set_password_label'))), this.setPassword() && m("input", { + className: "FormControl", + type: "password", + name: "password", + placeholder: (0,_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.password_label')), + bidi: this.password, + disabled: this.nonAdminEditingAdmin() + })), 20); + } + } + if (this.attrs.user.canEditGroups()) { + items.add('groups', m("div", { + className: "Form-group EditUserModal-groups" + }, m("label", null, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.groups_heading')), m("div", null, Object.keys(this.groups).map(id => _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].store.getById('groups', id)).filter(Boolean).map(group => + // Necessary because filter(Boolean) doesn't narrow out falsy values. + group && m("label", { + className: "checkbox" + }, m("input", { + type: "checkbox", + bidi: this.groups[group.id()], + disabled: group.id() === _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].ADMINISTRATOR_ID && (this.attrs.user === _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user || !this.userIsAdmin(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user)) + }), m(_GroupBadge__WEBPACK_IMPORTED_MODULE_4__["default"], { + group: group, + label: null + }), " ", group.nameSingular())))), 10); + } + items.add('submit', m("div", { + className: "Form-group Form-controls" + }, m(_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary", + type: "submit", + loading: this.loading + }, _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.edit_user.submit_button'))), -10); + return items; + } + activate() { + this.loading = true; + const data = { + username: this.username(), + isEmailConfirmed: true + }; + this.attrs.user.save(data, { + errorHandler: this.onerror.bind(this) + }).then(() => { + this.isEmailConfirmed(true); + this.loading = false; + m.redraw(); + }).catch(() => { + this.loading = false; + m.redraw(); + }); + } + data() { + const data = {}; + const relationships = {}; + if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) { + data.username = this.username(); + if (_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user !== this.attrs.user) { + data.email = this.email(); + } + if (this.setPassword()) { + data.password = this.password(); + } + } + if (this.attrs.user.canEditGroups()) { + relationships.groups = Object.keys(this.groups).filter(id => this.groups[id]()).map(id => _common_app__WEBPACK_IMPORTED_MODULE_1__["default"].store.getById('groups', id)).filter(g => g instanceof _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"]); + } + data.relationships = relationships; + return data; + } + onsubmit(e) { + e.preventDefault(); + this.loading = true; + this.attrs.user.save(this.data(), { + errorHandler: this.onerror.bind(this) + }).then(this.hide.bind(this)).catch(() => { + this.loading = false; + m.redraw(); + }); + } + nonAdminEditingAdmin() { + return this.userIsAdmin(this.attrs.user) && !this.userIsAdmin(_common_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user); + } + + /** + * @internal + */ + userIsAdmin(user) { + return !!((user == null ? void 0 : user.groups()) || []).some(g => (g == null ? void 0 : g.id()) === _models_Group__WEBPACK_IMPORTED_MODULE_5__["default"].ADMINISTRATOR_ID); + } +} +flarum.reg.add('core', 'common/components/EditUserModal', EditUserModal); + +/***/ }) + +}]); //# sourceMappingURL=EditUserModal.js.map \ No newline at end of file diff --git a/framework/core/js/dist/common/components/EditUserModal.js.map b/framework/core/js/dist/common/components/EditUserModal.js.map index 049690d77a4..2975a3a8774 100644 --- a/framework/core/js/dist/common/components/EditUserModal.js.map +++ b/framework/core/js/dist/common/components/EditUserModal.js.map @@ -1 +1 @@ -{"version":3,"file":"common/components/EditUserModal.js","mappings":"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","sources":["webpack://@flarum/core/./src/common/components/EditUserModal.tsx"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../common/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from './Button';\nimport GroupBadge from './GroupBadge';\nimport Group from '../models/Group';\nimport extractText from '../utils/extractText';\nimport ItemList from '../utils/ItemList';\nimport Stream from '../utils/Stream';\nimport Form from './Form';\nexport default class EditUserModal extends FormModal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"username\", void 0);\n _defineProperty(this, \"email\", void 0);\n _defineProperty(this, \"isEmailConfirmed\", void 0);\n _defineProperty(this, \"setPassword\", void 0);\n _defineProperty(this, \"password\", void 0);\n _defineProperty(this, \"groups\", {});\n }\n oninit(vnode) {\n super.oninit(vnode);\n const user = this.attrs.user;\n this.username = Stream(user.username() || '');\n this.email = Stream(user.email() || '');\n this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);\n this.setPassword = Stream(false);\n this.password = Stream(user.password() || '');\n const userGroups = user.groups() || [];\n app.store.all('groups').filter(group => ![Group.GUEST_ID, Group.MEMBER_ID].includes(group.id())).forEach(group => this.groups[group.id()] = Stream(userGroups.includes(group)));\n }\n className() {\n return 'EditUserModal Modal--small';\n }\n title() {\n return app.translator.trans('core.lib.edit_user.title');\n }\n content() {\n const fields = this.fields().toArray();\n return m(\"div\", {\n className: \"Modal-body\"\n }, fields.length > 1 ? m(Form, null, this.fields().toArray()) : app.translator.trans('core.lib.edit_user.nothing_available'));\n }\n fields() {\n const items = new ItemList();\n if (this.attrs.user.canEditCredentials()) {\n items.add('username', m(\"div\", {\n className: \"Form-group\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.username_heading')), m(\"input\", {\n className: \"FormControl\",\n placeholder: extractText(app.translator.trans('core.lib.edit_user.username_label')),\n bidi: this.username,\n disabled: this.nonAdminEditingAdmin()\n })), 40);\n if (app.session.user !== this.attrs.user) {\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.email_heading')), m(\"input\", {\n className: \"FormControl\",\n placeholder: extractText(app.translator.trans('core.lib.edit_user.email_label')),\n bidi: this.email,\n disabled: this.nonAdminEditingAdmin()\n }), !this.isEmailConfirmed() && this.userIsAdmin(app.session.user) && m(Button, {\n className: \"Button Button--block\",\n loading: this.loading,\n onclick: this.activate.bind(this)\n }, app.translator.trans('core.lib.edit_user.activate_button'))), 30);\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.password_heading')), m(\"div\", null, m(\"label\", {\n className: \"checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n onchange: e => {\n const target = e.target;\n this.setPassword(target.checked);\n m.redraw.sync();\n if (target.checked) this.$('[name=password]').select();\n e.redraw = false;\n },\n disabled: this.nonAdminEditingAdmin()\n }), app.translator.trans('core.lib.edit_user.set_password_label'))), this.setPassword() && m(\"input\", {\n className: \"FormControl\",\n type: \"password\",\n name: \"password\",\n placeholder: extractText(app.translator.trans('core.lib.edit_user.password_label')),\n bidi: this.password,\n disabled: this.nonAdminEditingAdmin()\n })), 20);\n }\n }\n if (this.attrs.user.canEditGroups()) {\n items.add('groups', m(\"div\", {\n className: \"Form-group EditUserModal-groups\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.groups_heading')), m(\"div\", null, Object.keys(this.groups).map(id => app.store.getById('groups', id)).filter(Boolean).map(group =>\n // Necessary because filter(Boolean) doesn't narrow out falsy values.\n group && m(\"label\", {\n className: \"checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n bidi: this.groups[group.id()],\n disabled: group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))\n }), m(GroupBadge, {\n group: group,\n label: null\n }), \" \", group.nameSingular())))), 10);\n }\n items.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.lib.edit_user.submit_button'))), -10);\n return items;\n }\n activate() {\n this.loading = true;\n const data = {\n username: this.username(),\n isEmailConfirmed: true\n };\n this.attrs.user.save(data, {\n errorHandler: this.onerror.bind(this)\n }).then(() => {\n this.isEmailConfirmed(true);\n this.loading = false;\n m.redraw();\n }).catch(() => {\n this.loading = false;\n m.redraw();\n });\n }\n data() {\n const data = {};\n const relationships = {};\n if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) {\n data.username = this.username();\n if (app.session.user !== this.attrs.user) {\n data.email = this.email();\n }\n if (this.setPassword()) {\n data.password = this.password();\n }\n }\n if (this.attrs.user.canEditGroups()) {\n relationships.groups = Object.keys(this.groups).filter(id => this.groups[id]()).map(id => app.store.getById('groups', id)).filter(g => g instanceof Group);\n }\n data.relationships = relationships;\n return data;\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n this.attrs.user.save(this.data(), {\n errorHandler: this.onerror.bind(this)\n }).then(this.hide.bind(this)).catch(() => {\n this.loading = false;\n m.redraw();\n });\n }\n nonAdminEditingAdmin() {\n return this.userIsAdmin(this.attrs.user) && !this.userIsAdmin(app.session.user);\n }\n\n /**\n * @internal\n */\n userIsAdmin(user) {\n return !!((user == null ? void 0 : user.groups()) || []).some(g => (g == null ? void 0 : g.id()) === Group.ADMINISTRATOR_ID);\n }\n}\nflarum.reg.add('core', 'common/components/EditUserModal', EditUserModal);"],"names":["EditUserModal","constructor","super","arguments","this","oninit","vnode","user","attrs","username","email","isEmailConfirmed","setPassword","password","userGroups","groups","filter","group","includes","id","forEach","className","title","content","fields","toArray","m","length","items","canEditCredentials","add","placeholder","bidi","disabled","nonAdminEditingAdmin","userIsAdmin","loading","onclick","activate","bind","type","onchange","e","target","checked","redraw","sync","$","select","name","canEditGroups","Object","keys","map","Boolean","label","nameSingular","data","save","errorHandler","onerror","then","catch","relationships","g","onsubmit","preventDefault","hide","some","flarum","reg"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"common/components/EditUserModal.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAwE;AACrC;AACuB;AAC5B;AACQ;AACF;AACW;AACN;AACJ;AACX;AACX,4BAA4B,oEAAS;AACpD;AACA;AACA,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB,IAAI,qFAAe,mBAAmB;AACtC;AACA;AACA;AACA;AACA,oBAAoB,yDAAM;AAC1B,iBAAiB,yDAAM;AACvB,4BAA4B,yDAAM;AAClC,uBAAuB,yDAAM;AAC7B,oBAAoB,yDAAM;AAC1B;AACA,IAAI,6DAAa,6BAA6B,8DAAc,EAAE,+DAAe,mEAAmE,yDAAM;AACtJ;AACA;AACA;AACA;AACA;AACA,WAAW,oEAAoB;AAC/B;AACA;AACA;AACA;AACA;AACA,KAAK,wBAAwB,6CAAI,mCAAmC,oEAAoB;AACxF;AACA;AACA,sBAAsB,uDAAQ;AAC9B;AACA;AACA;AACA,OAAO,mBAAmB,oEAAoB;AAC9C;AACA,qBAAqB,8DAAW,CAAC,oEAAoB;AACrD;AACA;AACA,OAAO;AACP,UAAU,gEAAgB;AAC1B;AACA;AACA,SAAS,mBAAmB,oEAAoB;AAChD;AACA,uBAAuB,8DAAW,CAAC,oEAAoB;AACvD;AACA;AACA,SAAS,gDAAgD,gEAAgB,OAAO,+CAAM;AACtF;AACA;AACA;AACA,SAAS,EAAE,oEAAoB;AAC/B;AACA;AACA,SAAS,mBAAmB,oEAAoB;AAChD;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW;AACX;AACA,SAAS,GAAG,oEAAoB;AAChC;AACA;AACA;AACA,uBAAuB,8DAAW,CAAC,oEAAoB;AACvD;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA,OAAO,mBAAmB,oEAAoB,0FAA0F,iEAAiB;AACzJ;AACA;AACA;AACA,OAAO;AACP;AACA;AACA,iCAAiC,sEAAsB,yBAAyB,gEAAgB,sBAAsB,gEAAgB;AACtI,OAAO,KAAK,mDAAU;AACtB;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,KAAK,IAAI,+CAAM;AACf;AACA;AACA;AACA,KAAK,EAAE,oEAAoB;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA,UAAU,gEAAgB;AAC1B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gGAAgG,iEAAiB,yCAAyC,qDAAK;AAC/J;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA,kEAAkE,gEAAgB;AAClF;;AAEA;AACA;AACA;AACA;AACA,yGAAyG,sEAAsB;AAC/H;AACA;AACA","sources":["webpack://@flarum/core/./src/common/components/EditUserModal.tsx"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../common/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from './Button';\nimport GroupBadge from './GroupBadge';\nimport Group from '../models/Group';\nimport extractText from '../utils/extractText';\nimport ItemList from '../utils/ItemList';\nimport Stream from '../utils/Stream';\nimport Form from './Form';\nexport default class EditUserModal extends FormModal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"username\", void 0);\n _defineProperty(this, \"email\", void 0);\n _defineProperty(this, \"isEmailConfirmed\", void 0);\n _defineProperty(this, \"setPassword\", void 0);\n _defineProperty(this, \"password\", void 0);\n _defineProperty(this, \"groups\", {});\n }\n oninit(vnode) {\n super.oninit(vnode);\n const user = this.attrs.user;\n this.username = Stream(user.username() || '');\n this.email = Stream(user.email() || '');\n this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);\n this.setPassword = Stream(false);\n this.password = Stream(user.password() || '');\n const userGroups = user.groups() || [];\n app.store.all('groups').filter(group => ![Group.GUEST_ID, Group.MEMBER_ID].includes(group.id())).forEach(group => this.groups[group.id()] = Stream(userGroups.includes(group)));\n }\n className() {\n return 'EditUserModal Modal--small';\n }\n title() {\n return app.translator.trans('core.lib.edit_user.title');\n }\n content() {\n const fields = this.fields().toArray();\n return m(\"div\", {\n className: \"Modal-body\"\n }, fields.length > 1 ? m(Form, null, this.fields().toArray()) : app.translator.trans('core.lib.edit_user.nothing_available'));\n }\n fields() {\n const items = new ItemList();\n if (this.attrs.user.canEditCredentials()) {\n items.add('username', m(\"div\", {\n className: \"Form-group\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.username_heading')), m(\"input\", {\n className: \"FormControl\",\n placeholder: extractText(app.translator.trans('core.lib.edit_user.username_label')),\n bidi: this.username,\n disabled: this.nonAdminEditingAdmin()\n })), 40);\n if (app.session.user !== this.attrs.user) {\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.email_heading')), m(\"input\", {\n className: \"FormControl\",\n placeholder: extractText(app.translator.trans('core.lib.edit_user.email_label')),\n bidi: this.email,\n disabled: this.nonAdminEditingAdmin()\n }), !this.isEmailConfirmed() && this.userIsAdmin(app.session.user) && m(Button, {\n className: \"Button Button--block\",\n loading: this.loading,\n onclick: this.activate.bind(this)\n }, app.translator.trans('core.lib.edit_user.activate_button'))), 30);\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.password_heading')), m(\"div\", null, m(\"label\", {\n className: \"checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n onchange: e => {\n const target = e.target;\n this.setPassword(target.checked);\n m.redraw.sync();\n if (target.checked) this.$('[name=password]').select();\n e.redraw = false;\n },\n disabled: this.nonAdminEditingAdmin()\n }), app.translator.trans('core.lib.edit_user.set_password_label'))), this.setPassword() && m(\"input\", {\n className: \"FormControl\",\n type: \"password\",\n name: \"password\",\n placeholder: extractText(app.translator.trans('core.lib.edit_user.password_label')),\n bidi: this.password,\n disabled: this.nonAdminEditingAdmin()\n })), 20);\n }\n }\n if (this.attrs.user.canEditGroups()) {\n items.add('groups', m(\"div\", {\n className: \"Form-group EditUserModal-groups\"\n }, m(\"label\", null, app.translator.trans('core.lib.edit_user.groups_heading')), m(\"div\", null, Object.keys(this.groups).map(id => app.store.getById('groups', id)).filter(Boolean).map(group =>\n // Necessary because filter(Boolean) doesn't narrow out falsy values.\n group && m(\"label\", {\n className: \"checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n bidi: this.groups[group.id()],\n disabled: group.id() === Group.ADMINISTRATOR_ID && (this.attrs.user === app.session.user || !this.userIsAdmin(app.session.user))\n }), m(GroupBadge, {\n group: group,\n label: null\n }), \" \", group.nameSingular())))), 10);\n }\n items.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.lib.edit_user.submit_button'))), -10);\n return items;\n }\n activate() {\n this.loading = true;\n const data = {\n username: this.username(),\n isEmailConfirmed: true\n };\n this.attrs.user.save(data, {\n errorHandler: this.onerror.bind(this)\n }).then(() => {\n this.isEmailConfirmed(true);\n this.loading = false;\n m.redraw();\n }).catch(() => {\n this.loading = false;\n m.redraw();\n });\n }\n data() {\n const data = {};\n const relationships = {};\n if (this.attrs.user.canEditCredentials() && !this.nonAdminEditingAdmin()) {\n data.username = this.username();\n if (app.session.user !== this.attrs.user) {\n data.email = this.email();\n }\n if (this.setPassword()) {\n data.password = this.password();\n }\n }\n if (this.attrs.user.canEditGroups()) {\n relationships.groups = Object.keys(this.groups).filter(id => this.groups[id]()).map(id => app.store.getById('groups', id)).filter(g => g instanceof Group);\n }\n data.relationships = relationships;\n return data;\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n this.attrs.user.save(this.data(), {\n errorHandler: this.onerror.bind(this)\n }).then(this.hide.bind(this)).catch(() => {\n this.loading = false;\n m.redraw();\n });\n }\n nonAdminEditingAdmin() {\n return this.userIsAdmin(this.attrs.user) && !this.userIsAdmin(app.session.user);\n }\n\n /**\n * @internal\n */\n userIsAdmin(user) {\n return !!((user == null ? void 0 : user.groups()) || []).some(g => (g == null ? void 0 : g.id()) === Group.ADMINISTRATOR_ID);\n }\n}\nflarum.reg.add('core', 'common/components/EditUserModal', EditUserModal);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum.js.LICENSE.txt b/framework/core/js/dist/forum.js.LICENSE.txt deleted file mode 100644 index da067cd7bba..00000000000 --- a/framework/core/js/dist/forum.js.LICENSE.txt +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * quantize.js Copyright 2008 Nick Rabinowitz. - * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php - */ - -/*! - * Sizzle CSS Selector Engine v2.3.6 - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://js.foundation/ - * - * Date: 2021-02-16 - */ - -/*! - * Block below copied from Protovis: http://mbostock.github.com/protovis/ - * Copyright 2010 Stanford Visualization Group - * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php - */ - -/*! - * Color Thief v2.0 - * by Lokesh Dhakar - http://www.lokeshdhakar.com - * - * Thanks - * ------ - * Nick Rabinowitz - For creating quantize.js. - * John Schulz - For clean up and optimization. @JFSIII - * Nathan Spady - For adding drag and drop support to the demo page. - * - * License - * ------- - * Copyright 2011, 2015 Lokesh Dhakar - * Released under the MIT license - * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE - * - */ - -/*! - * jQuery JavaScript Library v3.6.1 - * https://jquery.com/ - * - * Includes Sizzle.js - * https://sizzlejs.com/ - * - * Copyright OpenJS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2022-08-26T17:52Z - */ - -/*! -* focus-trap 6.9.4 -* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE -*/ - -/*! -* tabbable 5.3.3 -* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE -*/ diff --git a/framework/core/js/dist/forum/components/Composer.js b/framework/core/js/dist/forum/components/Composer.js index f8866d9df3b..df4207874ac 100644 --- a/framework/core/js/dist/forum/components/Composer.js +++ b/framework/core/js/dist/forum/components/Composer.js @@ -1,2 +1,418 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[899],{6046:(t,s,i)=>{i.r(s),i.d(s,{default:()=>p});var e=i(6789),o=i(2190),n=i(4041),h=i(8312);class a extends h.Z{static initAttrs(t){super.initAttrs(t),t.className=t.className||"Button Button--icon Button--link"}}flarum.reg.add("core","forum/components/ComposerButton",a);var r=i(1268),c=i(3344),d=i(2401);class p extends o.Z{oninit(t){super.oninit(t),this.state=this.attrs.state,this.active=!1,this.prevPosition=this.state.position}view(){const t=this.state.body,s={normal:this.state.position===d.Z.Position.NORMAL,minimized:this.state.position===d.Z.Position.MINIMIZED,fullScreen:this.state.position===d.Z.Position.FULLSCREEN,active:this.active,visible:this.state.isVisible()},i=this.state.position===d.Z.Position.MINIMIZED?this.state.show.bind(this.state):void 0,e=t.componentClass;return m("div",{className:"Composer "+(0,c.Z)(s)},m("div",{className:"Composer-handle",oncreate:this.configHandle.bind(this)}),m("ul",{className:"Composer-controls"},(0,r.Z)(this.controlItems().toArray())),m("div",{className:"Composer-content",onclick:i},e&&m(e,Object.assign({},t.attrs,{composer:this.state,disabled:s.minimized}))))}onupdate(t){super.onupdate(t),this.state.position===this.prevPosition?this.updateHeight():(this.animatePositionChange(),this.prevPosition=this.state.position)}oncreate(t){super.oncreate(t),this.initializeHeight(),this.$().hide().css("bottom",-this.state.computedHeight()),this.$().on("focus blur",":input,.TextEditor-editorContainer",(t=>{this.active="focusin"===t.type,m.redraw()})),this.$().on("keydown",":input,.TextEditor-editorContainer","esc",(()=>this.state.close())),this.handlers={},$(window).on("resize",this.handlers.onresize=this.updateHeight.bind(this)).resize(),$(document).on("mousemove",this.handlers.onmousemove=this.onmousemove.bind(this)).on("mouseup",this.handlers.onmouseup=this.onmouseup.bind(this))}onremove(t){super.onremove(t),$(window).off("resize",this.handlers.onresize),$(document).off("mousemove",this.handlers.onmousemove).off("mouseup",this.handlers.onmouseup)}configHandle(t){const s=this;$(t.dom).css("cursor","row-resize").bind("dragstart mousedown",(t=>t.preventDefault())).mousedown((function(t){s.mouseStart=t.clientY,s.heightStart=s.$().height(),s.handle=$(this),$("body").css("cursor","row-resize")}))}onmousemove(t){if(!this.handle)return;const s=this.mouseStart-t.clientY;this.changeHeight(this.heightStart+s);const i=$(window).scrollTop(),e=i>0&&i+$(window).height()>=$(document).height();this.updateBodyPadding(e)}onmouseup(){this.handle&&(this.handle=null,$("body").css("cursor",""))}focus(){this.$(".Composer-content :input:enabled:visible, .TextEditor-editor").first().focus()}updateHeight(){const t=this.state.computedHeight(),s=this.$(".Composer-flexible");if(this.$().height(t),s.length){const t=s.offset().top-this.$().offset().top,i=parseInt(s.css("padding-bottom"),10),e=this.$(".Composer-footer").outerHeight(!0);s.height(this.$().outerHeight()-t-i-e)}}updateBodyPadding(){const t=this.state.position!==d.Z.Position.HIDDEN&&this.state.position!==d.Z.Position.MINIMIZED&&"phone"!==e.Z.screen()?this.state.computedHeight()-parseInt($("#app").css("padding-bottom"),10):0;$("#content").css({paddingBottom:t})}animatePositionChange(){if(this.prevPosition!==d.Z.Position.FULLSCREEN||this.state.position!==d.Z.Position.NORMAL)switch(this.state.position){case d.Z.Position.HIDDEN:return this.hide();case d.Z.Position.MINIMIZED:return this.minimize();case d.Z.Position.FULLSCREEN:return this.focus();case d.Z.Position.NORMAL:return this.show()}else this.focus()}animateHeightChange(){const t=this.$().stop(!0),s=t.outerHeight(),i=$(window).scrollTop();t.show(),this.updateHeight();const e=t.outerHeight();this.prevPosition===d.Z.Position.HIDDEN?t.css({bottom:-e,height:e}):t.css({height:s});const o=t.animate({bottom:0,height:e},"fast").promise();return this.updateBodyPadding(),$(window).scrollTop(i),o}showBackdrop(){this.$backdrop=$("
").addClass("composer-backdrop").appendTo("body")}hideBackdrop(){this.$backdrop&&this.$backdrop.remove()}show(){if(this.animateHeightChange().then((()=>this.focus())),"phone"===e.Z.screen()){const t=document.documentElement,s=Math.min(t.scrollTop,t.scrollHeight-t.clientHeight);this.$().css("top",$(".App").is(".mobile-safari")?s:0),this.showBackdrop()}}hide(){const t=this.$();t.stop(!0).animate({bottom:-t.height()},"fast",(()=>{t.hide(),this.hideBackdrop(),this.updateBodyPadding()}))}minimize(){this.animateHeightChange(),this.$().css("top","auto"),this.hideBackdrop()}controlItems(){const t=new n.Z;return this.state.position===d.Z.Position.FULLSCREEN?t.add("exitFullScreen",m(a,{icon:"fas fa-compress",title:e.Z.translator.trans("core.forum.composer.exit_full_screen_tooltip"),onclick:this.state.exitFullScreen.bind(this.state)})):(this.state.position!==d.Z.Position.MINIMIZED&&(t.add("minimize",m(a,{icon:(0,c.Z)("fas minimize",{"fa-minus":"phone"!==e.Z.screen(),"fa-times":"phone"===e.Z.screen()}),title:e.Z.translator.trans("core.forum.composer.minimize_tooltip"),onclick:this.state.minimize.bind(this.state),itemClassName:"App-backControl"})),t.add("fullScreen",m(a,{icon:"fas fa-expand",title:e.Z.translator.trans("core.forum.composer.full_screen_tooltip"),onclick:this.state.fullScreen.bind(this.state)}))),t.add("close",m(a,{icon:"fas fa-times",title:e.Z.translator.trans("core.forum.composer.close_tooltip"),onclick:this.state.close.bind(this.state)}))),t}initializeHeight(){this.state.height=localStorage.getItem("composerHeight"),this.state.height||(this.state.height=this.defaultHeight())}defaultHeight(){return this.$().height()}changeHeight(t){this.state.height=t,this.updateHeight(),localStorage.setItem("composerHeight",this.state.height)}}flarum.reg.add("core","forum/components/Composer",p)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/Composer"],{ + +/***/ "./src/forum/components/Composer.js": +/*!******************************************!*\ + !*** ./src/forum/components/Composer.js ***! + \******************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ Composer) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _ComposerButton__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./ComposerButton */ "./src/forum/components/ComposerButton.js"); +/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx"); +/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts"); +/* harmony import */ var _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../states/ComposerState */ "./src/forum/states/ComposerState.js"); + + + + + + + + +/** + * The `Composer` component displays the composer. It can be loaded with a + * content component with `load` and then its position/state can be altered with + * `show`, `hide`, `close`, `minimize`, `fullScreen`, and `exitFullScreen`. + */ +class Composer extends _common_Component__WEBPACK_IMPORTED_MODULE_1__["default"] { + oninit(vnode) { + super.oninit(vnode); + + /** + * The composer's "state". + * + * @type {ComposerState} + */ + this.state = this.attrs.state; + + /** + * Whether or not the composer currently has focus. + * + * @type {Boolean} + */ + this.active = false; + + // Store the initial position so that we can trigger animations correctly. + this.prevPosition = this.state.position; + } + view() { + const body = this.state.body; + const classes = { + normal: this.state.position === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.NORMAL, + minimized: this.state.position === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.MINIMIZED, + fullScreen: this.state.position === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.FULLSCREEN, + active: this.active, + visible: this.state.isVisible() + }; + + // Set up a handler so that clicks on the content will show the composer. + const showIfMinimized = this.state.position === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.MINIMIZED ? this.state.show.bind(this.state) : undefined; + const ComposerBody = body.componentClass; + return m("div", { + className: 'Composer ' + (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_5__["default"])(classes) + }, m("div", { + className: "Composer-handle", + oncreate: this.configHandle.bind(this) + }), m("ul", { + className: "Composer-controls" + }, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__["default"])(this.controlItems().toArray())), m("div", { + className: "Composer-content", + onclick: showIfMinimized + }, ComposerBody && m(ComposerBody, Object.assign({}, body.attrs, { + composer: this.state, + disabled: classes.minimized + })))); + } + onupdate(vnode) { + super.onupdate(vnode); + if (this.state.position === this.prevPosition) { + // Set the height of the Composer element and its contents on each redraw, + // so that they do not lose it if their DOM elements are recreated. + this.updateHeight(); + } else { + this.animatePositionChange(); + this.prevPosition = this.state.position; + } + } + oncreate(vnode) { + super.oncreate(vnode); + this.initializeHeight(); + this.$().hide().css('bottom', -this.state.computedHeight()); + + // Whenever any of the inputs inside the composer are have focus, we want to + // add a class to the composer to draw attention to it. + this.$().on('focus blur', ':input,.TextEditor-editorContainer', e => { + this.active = e.type === 'focusin'; + m.redraw(); + }); + + // When the escape key is pressed on any inputs, close the composer. + this.$().on('keydown', ':input,.TextEditor-editorContainer', 'esc', () => this.state.close()); + this.handlers = {}; + $(window).on('resize', this.handlers.onresize = this.updateHeight.bind(this)).resize(); + $(document).on('mousemove', this.handlers.onmousemove = this.onmousemove.bind(this)).on('mouseup', this.handlers.onmouseup = this.onmouseup.bind(this)); + } + onremove(vnode) { + super.onremove(vnode); + $(window).off('resize', this.handlers.onresize); + $(document).off('mousemove', this.handlers.onmousemove).off('mouseup', this.handlers.onmouseup); + } + + /** + * Add the necessary event handlers to the composer's handle so that it can + * be used to resize the composer. + */ + configHandle(vnode) { + const composer = this; + $(vnode.dom).css('cursor', 'row-resize').bind('dragstart mousedown', e => e.preventDefault()).mousedown(function (e) { + composer.mouseStart = e.clientY; + composer.heightStart = composer.$().height(); + composer.handle = $(this); + $('body').css('cursor', 'row-resize'); + }); + } + + /** + * Resize the composer according to mouse movement. + * + * @param {MouseEvent} e + */ + onmousemove(e) { + if (!this.handle) return; + + // Work out how much the mouse has been moved, and set the height + // relative to the old one based on that. Then update the content's + // height so that it fills the height of the composer, and update the + // body's padding. + const deltaPixels = this.mouseStart - e.clientY; + this.changeHeight(this.heightStart + deltaPixels); + + // Update the body's padding-bottom so that no content on the page will ever + // get permanently hidden behind the composer. If the user is already + // scrolled to the bottom of the page, then we will keep them scrolled to + // the bottom after the padding has been updated. + const scrollTop = $(window).scrollTop(); + const anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height(); + this.updateBodyPadding(anchorToBottom); + } + + /** + * Finish resizing the composer when the mouse is released. + */ + onmouseup() { + if (!this.handle) return; + this.handle = null; + $('body').css('cursor', ''); + } + + /** + * Draw focus to the first focusable content element (the text editor). + */ + focus() { + this.$('.Composer-content :input:enabled:visible, .TextEditor-editor').first().focus(); + } + + /** + * Update the DOM to reflect the composer's current height. This involves + * setting the height of the composer's root element, and adjusting the height + * of any flexible elements inside the composer's body. + */ + updateHeight() { + const height = this.state.computedHeight(); + const $flexible = this.$('.Composer-flexible'); + this.$().height(height); + if ($flexible.length) { + const headerHeight = $flexible.offset().top - this.$().offset().top; + const paddingBottom = parseInt($flexible.css('padding-bottom'), 10); + const footerHeight = this.$('.Composer-footer').outerHeight(true); + $flexible.height(this.$().outerHeight() - headerHeight - paddingBottom - footerHeight); + } + } + + /** + * Update the amount of padding-bottom on the body so that the page's + * content will still be visible above the composer when the page is + * scrolled right to the bottom. + */ + updateBodyPadding() { + const visible = this.state.position !== _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.HIDDEN && this.state.position !== _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.MINIMIZED && _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].screen() !== 'phone'; + const paddingBottom = visible ? this.state.computedHeight() - parseInt($('#app').css('padding-bottom'), 10) : 0; + $('#content').css({ + paddingBottom + }); + } + + /** + * Trigger the right animation depending on the desired new position. + */ + animatePositionChange() { + // When exiting full-screen mode: focus content + if (this.prevPosition === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.FULLSCREEN && this.state.position === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.NORMAL) { + this.focus(); + return; + } + switch (this.state.position) { + case _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.HIDDEN: + return this.hide(); + case _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.MINIMIZED: + return this.minimize(); + case _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.FULLSCREEN: + return this.focus(); + case _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.NORMAL: + return this.show(); + } + } + + /** + * Animate the Composer into the new position by changing the height. + */ + animateHeightChange() { + const $composer = this.$().stop(true); + const oldHeight = $composer.outerHeight(); + const scrollTop = $(window).scrollTop(); + $composer.show(); + this.updateHeight(); + const newHeight = $composer.outerHeight(); + if (this.prevPosition === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.HIDDEN) { + $composer.css({ + bottom: -newHeight, + height: newHeight + }); + } else { + $composer.css({ + height: oldHeight + }); + } + const animation = $composer.animate({ + bottom: 0, + height: newHeight + }, 'fast').promise(); + this.updateBodyPadding(); + $(window).scrollTop(scrollTop); + return animation; + } + + /** + * Show the Composer backdrop. + */ + showBackdrop() { + this.$backdrop = $('
').addClass('composer-backdrop').appendTo('body'); + } + + /** + * Hide the Composer backdrop. + */ + hideBackdrop() { + if (this.$backdrop) this.$backdrop.remove(); + } + + /** + * Animate the composer sliding up from the bottom to take its normal height. + * + * @private + */ + show() { + this.animateHeightChange().then(() => this.focus()); + if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].screen() === 'phone') { + // On safari fixed position doesn't properly work on mobile, + // So we use absolute and set the top value. + // https://github.com/flarum/core/issues/2652 + + // Due to another safari bug, `scrollTop` is unreliable when + // at the very bottom of the page AND opening the composer. + // So we fallback to a calculated version of scrollTop. + // https://github.com/flarum/core/issues/2683 + const scrollElement = document.documentElement; + const topOfViewport = Math.min(scrollElement.scrollTop, scrollElement.scrollHeight - scrollElement.clientHeight); + this.$().css('top', $('.App').is('.mobile-safari') ? topOfViewport : 0); + this.showBackdrop(); + } + } + + /** + * Animate closing the composer. + * + * @private + */ + hide() { + const $composer = this.$(); + + // Animate the composer sliding down off the bottom edge of the viewport. + // Only when the animation is completed, update other elements on the page. + $composer.stop(true).animate({ + bottom: -$composer.height() + }, 'fast', () => { + $composer.hide(); + this.hideBackdrop(); + this.updateBodyPadding(); + }); + } + + /** + * Shrink the composer until only its title is visible. + * + * @private + */ + minimize() { + this.animateHeightChange(); + this.$().css('top', 'auto'); + this.hideBackdrop(); + } + + /** + * Build an item list for the composer's controls. + * + * @return {ItemList} + */ + controlItems() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_2__["default"](); + if (this.state.position === _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.FULLSCREEN) { + items.add('exitFullScreen', m(_ComposerButton__WEBPACK_IMPORTED_MODULE_3__["default"], { + icon: "fas fa-compress", + title: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer.exit_full_screen_tooltip'), + onclick: this.state.exitFullScreen.bind(this.state) + })); + } else { + if (this.state.position !== _states_ComposerState__WEBPACK_IMPORTED_MODULE_6__["default"].Position.MINIMIZED) { + items.add('minimize', m(_ComposerButton__WEBPACK_IMPORTED_MODULE_3__["default"], { + icon: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_5__["default"])('fas minimize', { + 'fa-minus': _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].screen() !== 'phone', + 'fa-times': _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].screen() === 'phone' + }), + title: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer.minimize_tooltip'), + onclick: this.state.minimize.bind(this.state), + itemClassName: "App-backControl" + })); + items.add('fullScreen', m(_ComposerButton__WEBPACK_IMPORTED_MODULE_3__["default"], { + icon: "fas fa-expand", + title: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer.full_screen_tooltip'), + onclick: this.state.fullScreen.bind(this.state) + })); + } + items.add('close', m(_ComposerButton__WEBPACK_IMPORTED_MODULE_3__["default"], { + icon: "fas fa-times", + title: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer.close_tooltip'), + onclick: this.state.close.bind(this.state) + })); + } + return items; + } + + /** + * Initialize default Composer height. + */ + initializeHeight() { + this.state.height = localStorage.getItem('composerHeight'); + if (!this.state.height) { + this.state.height = this.defaultHeight(); + } + } + + /** + * Default height of the Composer in case none is saved. + * @returns {number} + */ + defaultHeight() { + return this.$().height(); + } + + /** + * Save a new Composer height and update the DOM. + * @param {number} height + */ + changeHeight(height) { + this.state.height = height; + this.updateHeight(); + localStorage.setItem('composerHeight', this.state.height); + } +} +flarum.reg.add('core', 'forum/components/Composer', Composer); + +/***/ }), + +/***/ "./src/forum/components/ComposerButton.js": +/*!************************************************!*\ + !*** ./src/forum/components/ComposerButton.js ***! + \************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ComposerButton) +/* harmony export */ }); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); + + +/** + * The `ComposerButton` component displays a button suitable for the composer + * controls. + */ +class ComposerButton extends _common_components_Button__WEBPACK_IMPORTED_MODULE_0__["default"] { + static initAttrs(attrs) { + super.initAttrs(attrs); + attrs.className = attrs.className || 'Button Button--icon Button--link'; + } +} +flarum.reg.add('core', 'forum/components/ComposerButton', ComposerButton); + +/***/ }) + +}]); //# sourceMappingURL=Composer.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/Composer.js.map b/framework/core/js/dist/forum/components/Composer.js.map index 51846733ed4..61774e88e91 100644 --- a/framework/core/js/dist/forum/components/Composer.js.map +++ b/framework/core/js/dist/forum/components/Composer.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/Composer.js","mappings":"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","sources":["webpack://@flarum/core/./src/forum/components/ComposerButton.js","webpack://@flarum/core/./src/forum/components/Composer.js"],"sourcesContent":["import Button from '../../common/components/Button';\n\n/**\n * The `ComposerButton` component displays a button suitable for the composer\n * controls.\n */\nexport default class ComposerButton extends Button {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.className = attrs.className || 'Button Button--icon Button--link';\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerButton', ComposerButton);","import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport ItemList from '../../common/utils/ItemList';\nimport ComposerButton from './ComposerButton';\nimport listItems from '../../common/helpers/listItems';\nimport classList from '../../common/utils/classList';\nimport ComposerState from '../states/ComposerState';\n\n/**\n * The `Composer` component displays the composer. It can be loaded with a\n * content component with `load` and then its position/state can be altered with\n * `show`, `hide`, `close`, `minimize`, `fullScreen`, and `exitFullScreen`.\n */\nexport default class Composer extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n\n /**\n * The composer's \"state\".\n *\n * @type {ComposerState}\n */\n this.state = this.attrs.state;\n\n /**\n * Whether or not the composer currently has focus.\n *\n * @type {Boolean}\n */\n this.active = false;\n\n // Store the initial position so that we can trigger animations correctly.\n this.prevPosition = this.state.position;\n }\n view() {\n const body = this.state.body;\n const classes = {\n normal: this.state.position === ComposerState.Position.NORMAL,\n minimized: this.state.position === ComposerState.Position.MINIMIZED,\n fullScreen: this.state.position === ComposerState.Position.FULLSCREEN,\n active: this.active,\n visible: this.state.isVisible()\n };\n\n // Set up a handler so that clicks on the content will show the composer.\n const showIfMinimized = this.state.position === ComposerState.Position.MINIMIZED ? this.state.show.bind(this.state) : undefined;\n const ComposerBody = body.componentClass;\n return m(\"div\", {\n className: 'Composer ' + classList(classes)\n }, m(\"div\", {\n className: \"Composer-handle\",\n oncreate: this.configHandle.bind(this)\n }), m(\"ul\", {\n className: \"Composer-controls\"\n }, listItems(this.controlItems().toArray())), m(\"div\", {\n className: \"Composer-content\",\n onclick: showIfMinimized\n }, ComposerBody && m(ComposerBody, Object.assign({}, body.attrs, {\n composer: this.state,\n disabled: classes.minimized\n }))));\n }\n onupdate(vnode) {\n super.onupdate(vnode);\n if (this.state.position === this.prevPosition) {\n // Set the height of the Composer element and its contents on each redraw,\n // so that they do not lose it if their DOM elements are recreated.\n this.updateHeight();\n } else {\n this.animatePositionChange();\n this.prevPosition = this.state.position;\n }\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.initializeHeight();\n this.$().hide().css('bottom', -this.state.computedHeight());\n\n // Whenever any of the inputs inside the composer are have focus, we want to\n // add a class to the composer to draw attention to it.\n this.$().on('focus blur', ':input,.TextEditor-editorContainer', e => {\n this.active = e.type === 'focusin';\n m.redraw();\n });\n\n // When the escape key is pressed on any inputs, close the composer.\n this.$().on('keydown', ':input,.TextEditor-editorContainer', 'esc', () => this.state.close());\n this.handlers = {};\n $(window).on('resize', this.handlers.onresize = this.updateHeight.bind(this)).resize();\n $(document).on('mousemove', this.handlers.onmousemove = this.onmousemove.bind(this)).on('mouseup', this.handlers.onmouseup = this.onmouseup.bind(this));\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('resize', this.handlers.onresize);\n $(document).off('mousemove', this.handlers.onmousemove).off('mouseup', this.handlers.onmouseup);\n }\n\n /**\n * Add the necessary event handlers to the composer's handle so that it can\n * be used to resize the composer.\n */\n configHandle(vnode) {\n const composer = this;\n $(vnode.dom).css('cursor', 'row-resize').bind('dragstart mousedown', e => e.preventDefault()).mousedown(function (e) {\n composer.mouseStart = e.clientY;\n composer.heightStart = composer.$().height();\n composer.handle = $(this);\n $('body').css('cursor', 'row-resize');\n });\n }\n\n /**\n * Resize the composer according to mouse movement.\n *\n * @param {MouseEvent} e\n */\n onmousemove(e) {\n if (!this.handle) return;\n\n // Work out how much the mouse has been moved, and set the height\n // relative to the old one based on that. Then update the content's\n // height so that it fills the height of the composer, and update the\n // body's padding.\n const deltaPixels = this.mouseStart - e.clientY;\n this.changeHeight(this.heightStart + deltaPixels);\n\n // Update the body's padding-bottom so that no content on the page will ever\n // get permanently hidden behind the composer. If the user is already\n // scrolled to the bottom of the page, then we will keep them scrolled to\n // the bottom after the padding has been updated.\n const scrollTop = $(window).scrollTop();\n const anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height();\n this.updateBodyPadding(anchorToBottom);\n }\n\n /**\n * Finish resizing the composer when the mouse is released.\n */\n onmouseup() {\n if (!this.handle) return;\n this.handle = null;\n $('body').css('cursor', '');\n }\n\n /**\n * Draw focus to the first focusable content element (the text editor).\n */\n focus() {\n this.$('.Composer-content :input:enabled:visible, .TextEditor-editor').first().focus();\n }\n\n /**\n * Update the DOM to reflect the composer's current height. This involves\n * setting the height of the composer's root element, and adjusting the height\n * of any flexible elements inside the composer's body.\n */\n updateHeight() {\n const height = this.state.computedHeight();\n const $flexible = this.$('.Composer-flexible');\n this.$().height(height);\n if ($flexible.length) {\n const headerHeight = $flexible.offset().top - this.$().offset().top;\n const paddingBottom = parseInt($flexible.css('padding-bottom'), 10);\n const footerHeight = this.$('.Composer-footer').outerHeight(true);\n $flexible.height(this.$().outerHeight() - headerHeight - paddingBottom - footerHeight);\n }\n }\n\n /**\n * Update the amount of padding-bottom on the body so that the page's\n * content will still be visible above the composer when the page is\n * scrolled right to the bottom.\n */\n updateBodyPadding() {\n const visible = this.state.position !== ComposerState.Position.HIDDEN && this.state.position !== ComposerState.Position.MINIMIZED && app.screen() !== 'phone';\n const paddingBottom = visible ? this.state.computedHeight() - parseInt($('#app').css('padding-bottom'), 10) : 0;\n $('#content').css({\n paddingBottom\n });\n }\n\n /**\n * Trigger the right animation depending on the desired new position.\n */\n animatePositionChange() {\n // When exiting full-screen mode: focus content\n if (this.prevPosition === ComposerState.Position.FULLSCREEN && this.state.position === ComposerState.Position.NORMAL) {\n this.focus();\n return;\n }\n switch (this.state.position) {\n case ComposerState.Position.HIDDEN:\n return this.hide();\n case ComposerState.Position.MINIMIZED:\n return this.minimize();\n case ComposerState.Position.FULLSCREEN:\n return this.focus();\n case ComposerState.Position.NORMAL:\n return this.show();\n }\n }\n\n /**\n * Animate the Composer into the new position by changing the height.\n */\n animateHeightChange() {\n const $composer = this.$().stop(true);\n const oldHeight = $composer.outerHeight();\n const scrollTop = $(window).scrollTop();\n $composer.show();\n this.updateHeight();\n const newHeight = $composer.outerHeight();\n if (this.prevPosition === ComposerState.Position.HIDDEN) {\n $composer.css({\n bottom: -newHeight,\n height: newHeight\n });\n } else {\n $composer.css({\n height: oldHeight\n });\n }\n const animation = $composer.animate({\n bottom: 0,\n height: newHeight\n }, 'fast').promise();\n this.updateBodyPadding();\n $(window).scrollTop(scrollTop);\n return animation;\n }\n\n /**\n * Show the Composer backdrop.\n */\n showBackdrop() {\n this.$backdrop = $('
').addClass('composer-backdrop').appendTo('body');\n }\n\n /**\n * Hide the Composer backdrop.\n */\n hideBackdrop() {\n if (this.$backdrop) this.$backdrop.remove();\n }\n\n /**\n * Animate the composer sliding up from the bottom to take its normal height.\n *\n * @private\n */\n show() {\n this.animateHeightChange().then(() => this.focus());\n if (app.screen() === 'phone') {\n // On safari fixed position doesn't properly work on mobile,\n // So we use absolute and set the top value.\n // https://github.com/flarum/core/issues/2652\n\n // Due to another safari bug, `scrollTop` is unreliable when\n // at the very bottom of the page AND opening the composer.\n // So we fallback to a calculated version of scrollTop.\n // https://github.com/flarum/core/issues/2683\n const scrollElement = document.documentElement;\n const topOfViewport = Math.min(scrollElement.scrollTop, scrollElement.scrollHeight - scrollElement.clientHeight);\n this.$().css('top', $('.App').is('.mobile-safari') ? topOfViewport : 0);\n this.showBackdrop();\n }\n }\n\n /**\n * Animate closing the composer.\n *\n * @private\n */\n hide() {\n const $composer = this.$();\n\n // Animate the composer sliding down off the bottom edge of the viewport.\n // Only when the animation is completed, update other elements on the page.\n $composer.stop(true).animate({\n bottom: -$composer.height()\n }, 'fast', () => {\n $composer.hide();\n this.hideBackdrop();\n this.updateBodyPadding();\n });\n }\n\n /**\n * Shrink the composer until only its title is visible.\n *\n * @private\n */\n minimize() {\n this.animateHeightChange();\n this.$().css('top', 'auto');\n this.hideBackdrop();\n }\n\n /**\n * Build an item list for the composer's controls.\n *\n * @return {ItemList}\n */\n controlItems() {\n const items = new ItemList();\n if (this.state.position === ComposerState.Position.FULLSCREEN) {\n items.add('exitFullScreen', m(ComposerButton, {\n icon: \"fas fa-compress\",\n title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),\n onclick: this.state.exitFullScreen.bind(this.state)\n }));\n } else {\n if (this.state.position !== ComposerState.Position.MINIMIZED) {\n items.add('minimize', m(ComposerButton, {\n icon: classList('fas minimize', {\n 'fa-minus': app.screen() !== 'phone',\n 'fa-times': app.screen() === 'phone'\n }),\n title: app.translator.trans('core.forum.composer.minimize_tooltip'),\n onclick: this.state.minimize.bind(this.state),\n itemClassName: \"App-backControl\"\n }));\n items.add('fullScreen', m(ComposerButton, {\n icon: \"fas fa-expand\",\n title: app.translator.trans('core.forum.composer.full_screen_tooltip'),\n onclick: this.state.fullScreen.bind(this.state)\n }));\n }\n items.add('close', m(ComposerButton, {\n icon: \"fas fa-times\",\n title: app.translator.trans('core.forum.composer.close_tooltip'),\n onclick: this.state.close.bind(this.state)\n }));\n }\n return items;\n }\n\n /**\n * Initialize default Composer height.\n */\n initializeHeight() {\n this.state.height = localStorage.getItem('composerHeight');\n if (!this.state.height) {\n this.state.height = this.defaultHeight();\n }\n }\n\n /**\n * Default height of the Composer in case none is saved.\n * @returns {number}\n */\n defaultHeight() {\n return this.$().height();\n }\n\n /**\n * Save a new Composer height and update the DOM.\n * @param {number} height\n */\n changeHeight(height) {\n this.state.height = height;\n this.updateHeight();\n localStorage.setItem('composerHeight', this.state.height);\n }\n}\nflarum.reg.add('core', 'forum/components/Composer', Composer);"],"names":["ComposerButton","Button","static","attrs","super","initAttrs","className","flarum","reg","add","Composer","Component","oninit","vnode","this","state","active","prevPosition","position","view","body","classes","normal","ComposerState","minimized","fullScreen","visible","isVisible","showIfMinimized","show","bind","undefined","ComposerBody","componentClass","m","classList","oncreate","configHandle","listItems","controlItems","toArray","onclick","Object","assign","composer","disabled","onupdate","updateHeight","animatePositionChange","initializeHeight","$","hide","css","computedHeight","on","e","type","redraw","close","handlers","window","onresize","resize","document","onmousemove","onmouseup","onremove","off","dom","preventDefault","mousedown","mouseStart","clientY","heightStart","height","handle","deltaPixels","changeHeight","scrollTop","anchorToBottom","updateBodyPadding","focus","first","$flexible","length","headerHeight","offset","top","paddingBottom","parseInt","footerHeight","outerHeight","app","minimize","animateHeightChange","$composer","stop","oldHeight","newHeight","bottom","animation","animate","promise","showBackdrop","$backdrop","addClass","appendTo","hideBackdrop","remove","then","scrollElement","documentElement","topOfViewport","Math","min","scrollHeight","clientHeight","is","items","ItemList","icon","title","exitFullScreen","itemClassName","localStorage","getItem","defaultHeight","setItem"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/Composer.js","mappings":";;;;;;;;;;;;;;;;;;;;AAAkC;AACa;AACI;AACL;AACS;AACF;AACD;;AAEpD;AACA;AACA;AACA;AACA;AACe,uBAAuB,yDAAS;AAC/C;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,sCAAsC,6EAA6B;AACnE,yCAAyC,gFAAgC;AACzE,0CAA0C,iFAAiC;AAC3E;AACA;AACA;;AAEA;AACA,oDAAoD,gFAAgC;AACpF;AACA;AACA,+BAA+B,mEAAS;AACxC,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA,KAAK,EAAE,qEAAS;AAChB;AACA;AACA,KAAK,kDAAkD;AACvD;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,KAAK;;AAEL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,aAAa,YAAY;AACzB;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,4CAA4C,6EAA6B,4BAA4B,gFAAgC,IAAI,yDAAU;AACnJ;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA,8BAA8B,iFAAiC,4BAA4B,6EAA6B;AACxH;AACA;AACA;AACA;AACA,WAAW,6EAA6B;AACxC;AACA,WAAW,gFAAgC;AAC3C;AACA,WAAW,iFAAiC;AAC5C;AACA,WAAW,6EAA6B;AACxC;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA8B,6EAA6B;AAC3D;AACA;AACA;AACA,OAAO;AACP,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ,yDAAU;AAClB;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA,sBAAsB,8DAAQ;AAC9B,gCAAgC,iFAAiC;AACjE,oCAAoC,uDAAc;AAClD;AACA,eAAe,mEAAoB;AACnC;AACA,OAAO;AACP,MAAM;AACN,kCAAkC,gFAAgC;AAClE,gCAAgC,uDAAc;AAC9C,gBAAgB,mEAAS;AACzB,wBAAwB,yDAAU;AAClC,wBAAwB,yDAAU;AAClC,WAAW;AACX,iBAAiB,mEAAoB;AACrC;AACA;AACA,SAAS;AACT,kCAAkC,uDAAc;AAChD;AACA,iBAAiB,mEAAoB;AACrC;AACA,SAAS;AACT;AACA,2BAA2B,uDAAc;AACzC;AACA,eAAe,mEAAoB;AACnC;AACA,OAAO;AACP;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,eAAe;AACf;AACA;AACA;AACA;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;AC7WoD;;AAEpD;AACA;AACA;AACA;AACe,6BAA6B,iEAAM;AAClD;AACA;AACA;AACA;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/Composer.js","webpack://@flarum/core/./src/forum/components/ComposerButton.js"],"sourcesContent":["import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport ItemList from '../../common/utils/ItemList';\nimport ComposerButton from './ComposerButton';\nimport listItems from '../../common/helpers/listItems';\nimport classList from '../../common/utils/classList';\nimport ComposerState from '../states/ComposerState';\n\n/**\n * The `Composer` component displays the composer. It can be loaded with a\n * content component with `load` and then its position/state can be altered with\n * `show`, `hide`, `close`, `minimize`, `fullScreen`, and `exitFullScreen`.\n */\nexport default class Composer extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n\n /**\n * The composer's \"state\".\n *\n * @type {ComposerState}\n */\n this.state = this.attrs.state;\n\n /**\n * Whether or not the composer currently has focus.\n *\n * @type {Boolean}\n */\n this.active = false;\n\n // Store the initial position so that we can trigger animations correctly.\n this.prevPosition = this.state.position;\n }\n view() {\n const body = this.state.body;\n const classes = {\n normal: this.state.position === ComposerState.Position.NORMAL,\n minimized: this.state.position === ComposerState.Position.MINIMIZED,\n fullScreen: this.state.position === ComposerState.Position.FULLSCREEN,\n active: this.active,\n visible: this.state.isVisible()\n };\n\n // Set up a handler so that clicks on the content will show the composer.\n const showIfMinimized = this.state.position === ComposerState.Position.MINIMIZED ? this.state.show.bind(this.state) : undefined;\n const ComposerBody = body.componentClass;\n return m(\"div\", {\n className: 'Composer ' + classList(classes)\n }, m(\"div\", {\n className: \"Composer-handle\",\n oncreate: this.configHandle.bind(this)\n }), m(\"ul\", {\n className: \"Composer-controls\"\n }, listItems(this.controlItems().toArray())), m(\"div\", {\n className: \"Composer-content\",\n onclick: showIfMinimized\n }, ComposerBody && m(ComposerBody, Object.assign({}, body.attrs, {\n composer: this.state,\n disabled: classes.minimized\n }))));\n }\n onupdate(vnode) {\n super.onupdate(vnode);\n if (this.state.position === this.prevPosition) {\n // Set the height of the Composer element and its contents on each redraw,\n // so that they do not lose it if their DOM elements are recreated.\n this.updateHeight();\n } else {\n this.animatePositionChange();\n this.prevPosition = this.state.position;\n }\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.initializeHeight();\n this.$().hide().css('bottom', -this.state.computedHeight());\n\n // Whenever any of the inputs inside the composer are have focus, we want to\n // add a class to the composer to draw attention to it.\n this.$().on('focus blur', ':input,.TextEditor-editorContainer', e => {\n this.active = e.type === 'focusin';\n m.redraw();\n });\n\n // When the escape key is pressed on any inputs, close the composer.\n this.$().on('keydown', ':input,.TextEditor-editorContainer', 'esc', () => this.state.close());\n this.handlers = {};\n $(window).on('resize', this.handlers.onresize = this.updateHeight.bind(this)).resize();\n $(document).on('mousemove', this.handlers.onmousemove = this.onmousemove.bind(this)).on('mouseup', this.handlers.onmouseup = this.onmouseup.bind(this));\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('resize', this.handlers.onresize);\n $(document).off('mousemove', this.handlers.onmousemove).off('mouseup', this.handlers.onmouseup);\n }\n\n /**\n * Add the necessary event handlers to the composer's handle so that it can\n * be used to resize the composer.\n */\n configHandle(vnode) {\n const composer = this;\n $(vnode.dom).css('cursor', 'row-resize').bind('dragstart mousedown', e => e.preventDefault()).mousedown(function (e) {\n composer.mouseStart = e.clientY;\n composer.heightStart = composer.$().height();\n composer.handle = $(this);\n $('body').css('cursor', 'row-resize');\n });\n }\n\n /**\n * Resize the composer according to mouse movement.\n *\n * @param {MouseEvent} e\n */\n onmousemove(e) {\n if (!this.handle) return;\n\n // Work out how much the mouse has been moved, and set the height\n // relative to the old one based on that. Then update the content's\n // height so that it fills the height of the composer, and update the\n // body's padding.\n const deltaPixels = this.mouseStart - e.clientY;\n this.changeHeight(this.heightStart + deltaPixels);\n\n // Update the body's padding-bottom so that no content on the page will ever\n // get permanently hidden behind the composer. If the user is already\n // scrolled to the bottom of the page, then we will keep them scrolled to\n // the bottom after the padding has been updated.\n const scrollTop = $(window).scrollTop();\n const anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height();\n this.updateBodyPadding(anchorToBottom);\n }\n\n /**\n * Finish resizing the composer when the mouse is released.\n */\n onmouseup() {\n if (!this.handle) return;\n this.handle = null;\n $('body').css('cursor', '');\n }\n\n /**\n * Draw focus to the first focusable content element (the text editor).\n */\n focus() {\n this.$('.Composer-content :input:enabled:visible, .TextEditor-editor').first().focus();\n }\n\n /**\n * Update the DOM to reflect the composer's current height. This involves\n * setting the height of the composer's root element, and adjusting the height\n * of any flexible elements inside the composer's body.\n */\n updateHeight() {\n const height = this.state.computedHeight();\n const $flexible = this.$('.Composer-flexible');\n this.$().height(height);\n if ($flexible.length) {\n const headerHeight = $flexible.offset().top - this.$().offset().top;\n const paddingBottom = parseInt($flexible.css('padding-bottom'), 10);\n const footerHeight = this.$('.Composer-footer').outerHeight(true);\n $flexible.height(this.$().outerHeight() - headerHeight - paddingBottom - footerHeight);\n }\n }\n\n /**\n * Update the amount of padding-bottom on the body so that the page's\n * content will still be visible above the composer when the page is\n * scrolled right to the bottom.\n */\n updateBodyPadding() {\n const visible = this.state.position !== ComposerState.Position.HIDDEN && this.state.position !== ComposerState.Position.MINIMIZED && app.screen() !== 'phone';\n const paddingBottom = visible ? this.state.computedHeight() - parseInt($('#app').css('padding-bottom'), 10) : 0;\n $('#content').css({\n paddingBottom\n });\n }\n\n /**\n * Trigger the right animation depending on the desired new position.\n */\n animatePositionChange() {\n // When exiting full-screen mode: focus content\n if (this.prevPosition === ComposerState.Position.FULLSCREEN && this.state.position === ComposerState.Position.NORMAL) {\n this.focus();\n return;\n }\n switch (this.state.position) {\n case ComposerState.Position.HIDDEN:\n return this.hide();\n case ComposerState.Position.MINIMIZED:\n return this.minimize();\n case ComposerState.Position.FULLSCREEN:\n return this.focus();\n case ComposerState.Position.NORMAL:\n return this.show();\n }\n }\n\n /**\n * Animate the Composer into the new position by changing the height.\n */\n animateHeightChange() {\n const $composer = this.$().stop(true);\n const oldHeight = $composer.outerHeight();\n const scrollTop = $(window).scrollTop();\n $composer.show();\n this.updateHeight();\n const newHeight = $composer.outerHeight();\n if (this.prevPosition === ComposerState.Position.HIDDEN) {\n $composer.css({\n bottom: -newHeight,\n height: newHeight\n });\n } else {\n $composer.css({\n height: oldHeight\n });\n }\n const animation = $composer.animate({\n bottom: 0,\n height: newHeight\n }, 'fast').promise();\n this.updateBodyPadding();\n $(window).scrollTop(scrollTop);\n return animation;\n }\n\n /**\n * Show the Composer backdrop.\n */\n showBackdrop() {\n this.$backdrop = $('
').addClass('composer-backdrop').appendTo('body');\n }\n\n /**\n * Hide the Composer backdrop.\n */\n hideBackdrop() {\n if (this.$backdrop) this.$backdrop.remove();\n }\n\n /**\n * Animate the composer sliding up from the bottom to take its normal height.\n *\n * @private\n */\n show() {\n this.animateHeightChange().then(() => this.focus());\n if (app.screen() === 'phone') {\n // On safari fixed position doesn't properly work on mobile,\n // So we use absolute and set the top value.\n // https://github.com/flarum/core/issues/2652\n\n // Due to another safari bug, `scrollTop` is unreliable when\n // at the very bottom of the page AND opening the composer.\n // So we fallback to a calculated version of scrollTop.\n // https://github.com/flarum/core/issues/2683\n const scrollElement = document.documentElement;\n const topOfViewport = Math.min(scrollElement.scrollTop, scrollElement.scrollHeight - scrollElement.clientHeight);\n this.$().css('top', $('.App').is('.mobile-safari') ? topOfViewport : 0);\n this.showBackdrop();\n }\n }\n\n /**\n * Animate closing the composer.\n *\n * @private\n */\n hide() {\n const $composer = this.$();\n\n // Animate the composer sliding down off the bottom edge of the viewport.\n // Only when the animation is completed, update other elements on the page.\n $composer.stop(true).animate({\n bottom: -$composer.height()\n }, 'fast', () => {\n $composer.hide();\n this.hideBackdrop();\n this.updateBodyPadding();\n });\n }\n\n /**\n * Shrink the composer until only its title is visible.\n *\n * @private\n */\n minimize() {\n this.animateHeightChange();\n this.$().css('top', 'auto');\n this.hideBackdrop();\n }\n\n /**\n * Build an item list for the composer's controls.\n *\n * @return {ItemList}\n */\n controlItems() {\n const items = new ItemList();\n if (this.state.position === ComposerState.Position.FULLSCREEN) {\n items.add('exitFullScreen', m(ComposerButton, {\n icon: \"fas fa-compress\",\n title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),\n onclick: this.state.exitFullScreen.bind(this.state)\n }));\n } else {\n if (this.state.position !== ComposerState.Position.MINIMIZED) {\n items.add('minimize', m(ComposerButton, {\n icon: classList('fas minimize', {\n 'fa-minus': app.screen() !== 'phone',\n 'fa-times': app.screen() === 'phone'\n }),\n title: app.translator.trans('core.forum.composer.minimize_tooltip'),\n onclick: this.state.minimize.bind(this.state),\n itemClassName: \"App-backControl\"\n }));\n items.add('fullScreen', m(ComposerButton, {\n icon: \"fas fa-expand\",\n title: app.translator.trans('core.forum.composer.full_screen_tooltip'),\n onclick: this.state.fullScreen.bind(this.state)\n }));\n }\n items.add('close', m(ComposerButton, {\n icon: \"fas fa-times\",\n title: app.translator.trans('core.forum.composer.close_tooltip'),\n onclick: this.state.close.bind(this.state)\n }));\n }\n return items;\n }\n\n /**\n * Initialize default Composer height.\n */\n initializeHeight() {\n this.state.height = localStorage.getItem('composerHeight');\n if (!this.state.height) {\n this.state.height = this.defaultHeight();\n }\n }\n\n /**\n * Default height of the Composer in case none is saved.\n * @returns {number}\n */\n defaultHeight() {\n return this.$().height();\n }\n\n /**\n * Save a new Composer height and update the DOM.\n * @param {number} height\n */\n changeHeight(height) {\n this.state.height = height;\n this.updateHeight();\n localStorage.setItem('composerHeight', this.state.height);\n }\n}\nflarum.reg.add('core', 'forum/components/Composer', Composer);","import Button from '../../common/components/Button';\n\n/**\n * The `ComposerButton` component displays a button suitable for the composer\n * controls.\n */\nexport default class ComposerButton extends Button {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.className = attrs.className || 'Button Button--icon Button--link';\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerButton', ComposerButton);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/DiscussionComposer.js b/framework/core/js/dist/forum/components/DiscussionComposer.js index 29255248558..f33bb9ab2ac 100644 --- a/framework/core/js/dist/forum/components/DiscussionComposer.js +++ b/framework/core/js/dist/forum/components/DiscussionComposer.js @@ -1,2 +1,288 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[437],{2140:(s,e,t)=>{t.d(e,{Z:()=>h});var o=t(2190),i=t(5226);class r extends o.Z{handler(){return this.attrs.when()||void 0}oncreate(s){super.oncreate(s),this.boundHandler=this.handler.bind(this),$(window).on("beforeunload",this.boundHandler)}onremove(s){super.onremove(s),$(window).off("beforeunload",this.boundHandler)}view(s){return m("[",null,s.children)}}flarum.reg.add("core","common/components/ConfirmDocumentUnload",r);var n=t(4944),a=t(1268),d=t(4041),l=t(3344),c=t(7323);class h extends o.Z{oninit(s){super.oninit(s),this.composer=this.attrs.composer,this.loading=!1,this.attrs.confirmExit&&this.composer.preventClosingWhen((()=>this.hasChanges()),this.attrs.confirmExit),this.composer.fields.content(this.attrs.originalContent||"")}view(){var s;return m(r,{when:this.hasChanges.bind(this)},m("div",{className:(0,l.Z)("ComposerBody",this.attrs.className)},m(c.Z,{user:this.attrs.user,className:"ComposerBody-avatar"}),m("div",{className:"ComposerBody-content"},m("ul",{className:"ComposerBody-header"},(0,a.Z)(this.headerItems().toArray())),m("div",{className:"ComposerBody-editor"},m(n.Z,{submitLabel:this.attrs.submitLabel,placeholder:this.attrs.placeholder,disabled:this.loading||this.attrs.disabled,composer:this.composer,preview:null==(s=this.jumpToPreview)?void 0:s.bind(this),onchange:this.composer.fields.content,onsubmit:this.onsubmit.bind(this),value:this.composer.fields.content()}))),m(i.Z,{display:"unset",containerClassName:(0,l.Z)("ComposerBody-loading",this.loading&&"active"),size:"large"})))}hasChanges(){const s=this.composer.fields.content();return s&&s!==this.attrs.originalContent}headerItems(){return new d.Z}onsubmit(){}loaded(){this.loading=!1,m.redraw()}}flarum.reg.add("core","forum/components/ComposerBody",h)},3829:(s,e,t)=>{t.r(e),t.d(e,{default:()=>a});var o=t(6789),i=t(2140),r=t(1552),n=t(6458);class a extends i.Z{static initAttrs(s){super.initAttrs(s),s.placeholder=s.placeholder||(0,r.Z)(o.Z.translator.trans("core.forum.composer_discussion.body_placeholder")),s.submitLabel=s.submitLabel||o.Z.translator.trans("core.forum.composer_discussion.submit_button"),s.confirmExit=s.confirmExit||(0,r.Z)(o.Z.translator.trans("core.forum.composer_discussion.discard_confirmation")),s.titlePlaceholder=s.titlePlaceholder||(0,r.Z)(o.Z.translator.trans("core.forum.composer_discussion.title_placeholder")),s.className="ComposerBody--discussion"}oninit(s){super.oninit(s),this.composer.fields.title=this.composer.fields.title||(0,n.Z)(""),this.title=this.composer.fields.title}headerItems(){const s=super.headerItems();return s.add("title",m("h3",null,o.Z.translator.trans("core.forum.composer_discussion.title")),100),s.add("discussionTitle",m("h3",null,m("input",{className:"FormControl",bidi:this.title,placeholder:this.attrs.titlePlaceholder,disabled:!!this.attrs.disabled,onkeydown:this.onkeydown.bind(this)}))),s}onkeydown(s){13===s.which&&(s.preventDefault(),this.composer.editor.moveCursorTo(0)),s.redraw=!1}hasChanges(){return this.title()||this.composer.fields.content()}data(){return{title:this.title(),content:this.composer.fields.content()}}onsubmit(){this.loading=!0;const s=this.data();o.Z.store.createRecord("discussions").save(s).then((s=>{this.composer.hide(),o.Z.discussions.refresh(),m.route.set(o.Z.route.discussion(s))}),this.loaded.bind(this))}}flarum.reg.add("core","forum/components/DiscussionComposer",a)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/DiscussionComposer"],{ + +/***/ "./src/common/components/ConfirmDocumentUnload.js": +/*!********************************************************!*\ + !*** ./src/common/components/ConfirmDocumentUnload.js ***! + \********************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ConfirmDocumentUnload) +/* harmony export */ }); +/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ "./src/common/Component.ts"); + + +/** + * The `ConfirmDocumentUnload` component can be used to register a global + * event handler that prevents closing the browser window/tab based on the + * return value of a given callback prop. + * + * ### Attrs + * + * - `when` - a callback returning true when the browser should prompt for + * confirmation before closing the window/tab + */ +class ConfirmDocumentUnload extends _Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + handler() { + return this.attrs.when() || undefined; + } + oncreate(vnode) { + super.oncreate(vnode); + this.boundHandler = this.handler.bind(this); + $(window).on('beforeunload', this.boundHandler); + } + onremove(vnode) { + super.onremove(vnode); + $(window).off('beforeunload', this.boundHandler); + } + view(vnode) { + return m('[', null, vnode.children); + } +} +flarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload); + +/***/ }), + +/***/ "./src/forum/components/ComposerBody.js": +/*!**********************************************!*\ + !*** ./src/forum/components/ComposerBody.js ***! + \**********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ComposerBody) +/* harmony export */ }); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx"); +/* harmony import */ var _common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/ConfirmDocumentUnload */ "./src/common/components/ConfirmDocumentUnload.js"); +/* harmony import */ var _common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/TextEditor */ "./src/common/components/TextEditor.js"); +/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts"); +/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx"); + + + + + + + + + +/** + * The `ComposerBody` component handles the body, or the content, of the + * composer. Subclasses should implement the `onsubmit` method and override + * `headerTimes`. + * + * ### Attrs + * + * - `composer` + * - `originalContent` + * - `submitLabel` + * - `placeholder` + * - `user` + * - `confirmExit` + * - `disabled` + * + * @abstract + */ +class ComposerBody extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + oninit(vnode) { + super.oninit(vnode); + this.composer = this.attrs.composer; + + /** + * Whether or not the component is loading. + * + * @type {Boolean} + */ + this.loading = false; + + // Let the composer state know to ask for confirmation under certain + // circumstances, if the body supports / requires it and has a corresponding + // confirmation question to ask. + if (this.attrs.confirmExit) { + this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit); + } + this.composer.fields.content(this.attrs.originalContent || ''); + } + view() { + var _this$jumpToPreview; + return m(_common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__["default"], { + when: this.hasChanges.bind(this) + }, m("div", { + className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody', this.attrs.className) + }, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__["default"], { + user: this.attrs.user, + className: "ComposerBody-avatar" + }), m("div", { + className: "ComposerBody-content" + }, m("ul", { + className: "ComposerBody-header" + }, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__["default"])(this.headerItems().toArray())), m("div", { + className: "ComposerBody-editor" + }, m(_common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__["default"], { + submitLabel: this.attrs.submitLabel, + placeholder: this.attrs.placeholder, + disabled: this.loading || this.attrs.disabled, + composer: this.composer, + preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this), + onchange: this.composer.fields.content, + onsubmit: this.onsubmit.bind(this), + value: this.composer.fields.content() + }))), m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__["default"], { + display: "unset", + containerClassName: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody-loading', this.loading && 'active'), + size: "large" + }))); + } + + /** + * Check if there is any unsaved data. + * + * @return {boolean} + */ + hasChanges() { + const content = this.composer.fields.content(); + return content && content !== this.attrs.originalContent; + } + + /** + * Build an item list for the composer's header. + * + * @return {ItemList} + */ + headerItems() { + return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"](); + } + + /** + * Handle the submit event of the text editor. + * + * @abstract + */ + onsubmit() {} + + /** + * Stop loading. + */ + loaded() { + this.loading = false; + m.redraw(); + } +} +flarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody); + +/***/ }), + +/***/ "./src/forum/components/DiscussionComposer.js": +/*!****************************************************!*\ + !*** ./src/forum/components/DiscussionComposer.js ***! + \****************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ DiscussionComposer) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _ComposerBody__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ComposerBody */ "./src/forum/components/ComposerBody.js"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts"); + + + + + +/** + * The `DiscussionComposer` component displays the composer content for starting + * a new discussion. It adds a text field as a header control so the user can + * enter the title of their discussion. It also overrides the `submit` and + * `willExit` actions to account for the title. + * + * ### Attrs + * + * - All of the attrs for ComposerBody + * - `titlePlaceholder` + */ +class DiscussionComposer extends _ComposerBody__WEBPACK_IMPORTED_MODULE_1__["default"] { + static initAttrs(attrs) { + super.initAttrs(attrs); + attrs.placeholder = attrs.placeholder || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.body_placeholder')); + attrs.submitLabel = attrs.submitLabel || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.submit_button'); + attrs.confirmExit = attrs.confirmExit || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.discard_confirmation')); + attrs.titlePlaceholder = attrs.titlePlaceholder || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_2__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.title_placeholder')); + attrs.className = 'ComposerBody--discussion'; + } + oninit(vnode) { + super.oninit(vnode); + this.composer.fields.title = this.composer.fields.title || (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_3__["default"])(''); + + /** + * The value of the title input. + * + * @type {Function} + */ + this.title = this.composer.fields.title; + } + headerItems() { + const items = super.headerItems(); + items.add('title', m("h3", null, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_discussion.title')), 100); + items.add('discussionTitle', m("h3", null, m("input", { + className: "FormControl", + bidi: this.title, + placeholder: this.attrs.titlePlaceholder, + disabled: !!this.attrs.disabled, + onkeydown: this.onkeydown.bind(this) + }))); + return items; + } + + /** + * Handle the title input's keydown event. When the return key is pressed, + * move the focus to the start of the text editor. + * + * @param {KeyboardEvent} e + */ + onkeydown(e) { + if (e.which === 13) { + // Return + e.preventDefault(); + this.composer.editor.moveCursorTo(0); + } + e.redraw = false; + } + hasChanges() { + return this.title() || this.composer.fields.content(); + } + + /** + * Get the data to submit to the server when the discussion is saved. + * + * @return {Record} + */ + data() { + return { + title: this.title(), + content: this.composer.fields.content() + }; + } + onsubmit() { + this.loading = true; + const data = this.data(); + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].store.createRecord('discussions').save(data).then(discussion => { + this.composer.hide(); + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].discussions.refresh(); + m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(discussion)); + }, this.loaded.bind(this)); + } +} +flarum.reg.add('core', 'forum/components/DiscussionComposer', DiscussionComposer); + +/***/ }) + +}]); //# sourceMappingURL=DiscussionComposer.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/DiscussionComposer.js.map b/framework/core/js/dist/forum/components/DiscussionComposer.js.map index 6e4a6e78981..bae341e3072 100644 --- a/framework/core/js/dist/forum/components/DiscussionComposer.js.map +++ b/framework/core/js/dist/forum/components/DiscussionComposer.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/DiscussionComposer.js","mappings":"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","sources":["webpack://@flarum/core/./src/common/components/ConfirmDocumentUnload.js","webpack://@flarum/core/./src/forum/components/ComposerBody.js","webpack://@flarum/core/./src/forum/components/DiscussionComposer.js"],"sourcesContent":["import Component from '../Component';\n\n/**\n * The `ConfirmDocumentUnload` component can be used to register a global\n * event handler that prevents closing the browser window/tab based on the\n * return value of a given callback prop.\n *\n * ### Attrs\n *\n * - `when` - a callback returning true when the browser should prompt for\n * confirmation before closing the window/tab\n */\nexport default class ConfirmDocumentUnload extends Component {\n handler() {\n return this.attrs.when() || undefined;\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.boundHandler = this.handler.bind(this);\n $(window).on('beforeunload', this.boundHandler);\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('beforeunload', this.boundHandler);\n }\n view(vnode) {\n return m('[', null, vnode.children);\n }\n}\nflarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);","import Component from '../../common/Component';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';\nimport TextEditor from '../../common/components/TextEditor';\nimport listItems from '../../common/helpers/listItems';\nimport ItemList from '../../common/utils/ItemList';\nimport classList from '../../common/utils/classList';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ComposerBody` component handles the body, or the content, of the\n * composer. Subclasses should implement the `onsubmit` method and override\n * `headerTimes`.\n *\n * ### Attrs\n *\n * - `composer`\n * - `originalContent`\n * - `submitLabel`\n * - `placeholder`\n * - `user`\n * - `confirmExit`\n * - `disabled`\n *\n * @abstract\n */\nexport default class ComposerBody extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.composer = this.attrs.composer;\n\n /**\n * Whether or not the component is loading.\n *\n * @type {Boolean}\n */\n this.loading = false;\n\n // Let the composer state know to ask for confirmation under certain\n // circumstances, if the body supports / requires it and has a corresponding\n // confirmation question to ask.\n if (this.attrs.confirmExit) {\n this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);\n }\n this.composer.fields.content(this.attrs.originalContent || '');\n }\n view() {\n var _this$jumpToPreview;\n return m(ConfirmDocumentUnload, {\n when: this.hasChanges.bind(this)\n }, m(\"div\", {\n className: classList('ComposerBody', this.attrs.className)\n }, m(Avatar, {\n user: this.attrs.user,\n className: \"ComposerBody-avatar\"\n }), m(\"div\", {\n className: \"ComposerBody-content\"\n }, m(\"ul\", {\n className: \"ComposerBody-header\"\n }, listItems(this.headerItems().toArray())), m(\"div\", {\n className: \"ComposerBody-editor\"\n }, m(TextEditor, {\n submitLabel: this.attrs.submitLabel,\n placeholder: this.attrs.placeholder,\n disabled: this.loading || this.attrs.disabled,\n composer: this.composer,\n preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),\n onchange: this.composer.fields.content,\n onsubmit: this.onsubmit.bind(this),\n value: this.composer.fields.content()\n }))), m(LoadingIndicator, {\n display: \"unset\",\n containerClassName: classList('ComposerBody-loading', this.loading && 'active'),\n size: \"large\"\n })));\n }\n\n /**\n * Check if there is any unsaved data.\n *\n * @return {boolean}\n */\n hasChanges() {\n const content = this.composer.fields.content();\n return content && content !== this.attrs.originalContent;\n }\n\n /**\n * Build an item list for the composer's header.\n *\n * @return {ItemList}\n */\n headerItems() {\n return new ItemList();\n }\n\n /**\n * Handle the submit event of the text editor.\n *\n * @abstract\n */\n onsubmit() {}\n\n /**\n * Stop loading.\n */\n loaded() {\n this.loading = false;\n m.redraw();\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);","import app from '../../forum/app';\nimport ComposerBody from './ComposerBody';\nimport extractText from '../../common/utils/extractText';\nimport Stream from '../../common/utils/Stream';\n\n/**\n * The `DiscussionComposer` component displays the composer content for starting\n * a new discussion. It adds a text field as a header control so the user can\n * enter the title of their discussion. It also overrides the `submit` and\n * `willExit` actions to account for the title.\n *\n * ### Attrs\n *\n * - All of the attrs for ComposerBody\n * - `titlePlaceholder`\n */\nexport default class DiscussionComposer extends ComposerBody {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.placeholder = attrs.placeholder || extractText(app.translator.trans('core.forum.composer_discussion.body_placeholder'));\n attrs.submitLabel = attrs.submitLabel || app.translator.trans('core.forum.composer_discussion.submit_button');\n attrs.confirmExit = attrs.confirmExit || extractText(app.translator.trans('core.forum.composer_discussion.discard_confirmation'));\n attrs.titlePlaceholder = attrs.titlePlaceholder || extractText(app.translator.trans('core.forum.composer_discussion.title_placeholder'));\n attrs.className = 'ComposerBody--discussion';\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.composer.fields.title = this.composer.fields.title || Stream('');\n\n /**\n * The value of the title input.\n *\n * @type {Function}\n */\n this.title = this.composer.fields.title;\n }\n headerItems() {\n const items = super.headerItems();\n items.add('title', m(\"h3\", null, app.translator.trans('core.forum.composer_discussion.title')), 100);\n items.add('discussionTitle', m(\"h3\", null, m(\"input\", {\n className: \"FormControl\",\n bidi: this.title,\n placeholder: this.attrs.titlePlaceholder,\n disabled: !!this.attrs.disabled,\n onkeydown: this.onkeydown.bind(this)\n })));\n return items;\n }\n\n /**\n * Handle the title input's keydown event. When the return key is pressed,\n * move the focus to the start of the text editor.\n *\n * @param {KeyboardEvent} e\n */\n onkeydown(e) {\n if (e.which === 13) {\n // Return\n e.preventDefault();\n this.composer.editor.moveCursorTo(0);\n }\n e.redraw = false;\n }\n hasChanges() {\n return this.title() || this.composer.fields.content();\n }\n\n /**\n * Get the data to submit to the server when the discussion is saved.\n *\n * @return {Record}\n */\n data() {\n return {\n title: this.title(),\n content: this.composer.fields.content()\n };\n }\n onsubmit() {\n this.loading = true;\n const data = this.data();\n app.store.createRecord('discussions').save(data).then(discussion => {\n this.composer.hide();\n app.discussions.refresh();\n m.route.set(app.route.discussion(discussion));\n }, this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/DiscussionComposer', DiscussionComposer);"],"names":["ConfirmDocumentUnload","Component","handler","this","attrs","when","undefined","oncreate","vnode","super","boundHandler","bind","$","window","on","onremove","off","view","m","children","flarum","reg","add","ComposerBody","oninit","composer","loading","confirmExit","preventClosingWhen","hasChanges","fields","content","originalContent","_this$jumpToPreview","className","classList","Avatar","user","listItems","headerItems","toArray","TextEditor","submitLabel","placeholder","disabled","preview","jumpToPreview","onchange","onsubmit","value","LoadingIndicator","display","containerClassName","size","ItemList","loaded","redraw","DiscussionComposer","static","initAttrs","titlePlaceholder","title","items","bidi","onkeydown","e","which","preventDefault","editor","moveCursorTo","data","save","then","discussion","hide","route","set"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/DiscussionComposer.js","mappings":";;;;;;;;;;;;;;AAAqC;;AAErC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,oCAAoC,kDAAS;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC7B+C;AACyB;AACU;AACtB;AACL;AACJ;AACE;AACD;;AAEpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,2BAA2B,yDAAS;AACnD;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,gFAAqB;AAClC;AACA,KAAK;AACL,iBAAiB,mEAAS;AAC1B,KAAK,IAAI,iEAAM;AACf;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK,EAAE,qEAAS;AAChB;AACA,KAAK,IAAI,qEAAU;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,OAAO,2EAAgB;AAC5B;AACA,0BAA0B,mEAAS;AACnC;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA,eAAe,8DAAQ;AACvB;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;AC/GkC;AACQ;AACe;AACV;;AAE/C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,iCAAiC,qDAAY;AAC5D;AACA;AACA,6CAA6C,qEAAW,CAAC,mEAAoB;AAC7E,6CAA6C,mEAAoB;AACjE,6CAA6C,qEAAW,CAAC,mEAAoB;AAC7E,uDAAuD,qEAAW,CAAC,mEAAoB;AACvF;AACA;AACA;AACA;AACA,+DAA+D,gEAAM;;AAErE;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA,qCAAqC,mEAAoB;AACzD;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa,eAAe;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,qEAAsB;AAC1B;AACA,MAAM,sEAAuB;AAC7B,kBAAkB,mEAAoB;AACtC,KAAK;AACL;AACA;AACA","sources":["webpack://@flarum/core/./src/common/components/ConfirmDocumentUnload.js","webpack://@flarum/core/./src/forum/components/ComposerBody.js","webpack://@flarum/core/./src/forum/components/DiscussionComposer.js"],"sourcesContent":["import Component from '../Component';\n\n/**\n * The `ConfirmDocumentUnload` component can be used to register a global\n * event handler that prevents closing the browser window/tab based on the\n * return value of a given callback prop.\n *\n * ### Attrs\n *\n * - `when` - a callback returning true when the browser should prompt for\n * confirmation before closing the window/tab\n */\nexport default class ConfirmDocumentUnload extends Component {\n handler() {\n return this.attrs.when() || undefined;\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.boundHandler = this.handler.bind(this);\n $(window).on('beforeunload', this.boundHandler);\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('beforeunload', this.boundHandler);\n }\n view(vnode) {\n return m('[', null, vnode.children);\n }\n}\nflarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);","import Component from '../../common/Component';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';\nimport TextEditor from '../../common/components/TextEditor';\nimport listItems from '../../common/helpers/listItems';\nimport ItemList from '../../common/utils/ItemList';\nimport classList from '../../common/utils/classList';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ComposerBody` component handles the body, or the content, of the\n * composer. Subclasses should implement the `onsubmit` method and override\n * `headerTimes`.\n *\n * ### Attrs\n *\n * - `composer`\n * - `originalContent`\n * - `submitLabel`\n * - `placeholder`\n * - `user`\n * - `confirmExit`\n * - `disabled`\n *\n * @abstract\n */\nexport default class ComposerBody extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.composer = this.attrs.composer;\n\n /**\n * Whether or not the component is loading.\n *\n * @type {Boolean}\n */\n this.loading = false;\n\n // Let the composer state know to ask for confirmation under certain\n // circumstances, if the body supports / requires it and has a corresponding\n // confirmation question to ask.\n if (this.attrs.confirmExit) {\n this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);\n }\n this.composer.fields.content(this.attrs.originalContent || '');\n }\n view() {\n var _this$jumpToPreview;\n return m(ConfirmDocumentUnload, {\n when: this.hasChanges.bind(this)\n }, m(\"div\", {\n className: classList('ComposerBody', this.attrs.className)\n }, m(Avatar, {\n user: this.attrs.user,\n className: \"ComposerBody-avatar\"\n }), m(\"div\", {\n className: \"ComposerBody-content\"\n }, m(\"ul\", {\n className: \"ComposerBody-header\"\n }, listItems(this.headerItems().toArray())), m(\"div\", {\n className: \"ComposerBody-editor\"\n }, m(TextEditor, {\n submitLabel: this.attrs.submitLabel,\n placeholder: this.attrs.placeholder,\n disabled: this.loading || this.attrs.disabled,\n composer: this.composer,\n preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),\n onchange: this.composer.fields.content,\n onsubmit: this.onsubmit.bind(this),\n value: this.composer.fields.content()\n }))), m(LoadingIndicator, {\n display: \"unset\",\n containerClassName: classList('ComposerBody-loading', this.loading && 'active'),\n size: \"large\"\n })));\n }\n\n /**\n * Check if there is any unsaved data.\n *\n * @return {boolean}\n */\n hasChanges() {\n const content = this.composer.fields.content();\n return content && content !== this.attrs.originalContent;\n }\n\n /**\n * Build an item list for the composer's header.\n *\n * @return {ItemList}\n */\n headerItems() {\n return new ItemList();\n }\n\n /**\n * Handle the submit event of the text editor.\n *\n * @abstract\n */\n onsubmit() {}\n\n /**\n * Stop loading.\n */\n loaded() {\n this.loading = false;\n m.redraw();\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);","import app from '../../forum/app';\nimport ComposerBody from './ComposerBody';\nimport extractText from '../../common/utils/extractText';\nimport Stream from '../../common/utils/Stream';\n\n/**\n * The `DiscussionComposer` component displays the composer content for starting\n * a new discussion. It adds a text field as a header control so the user can\n * enter the title of their discussion. It also overrides the `submit` and\n * `willExit` actions to account for the title.\n *\n * ### Attrs\n *\n * - All of the attrs for ComposerBody\n * - `titlePlaceholder`\n */\nexport default class DiscussionComposer extends ComposerBody {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.placeholder = attrs.placeholder || extractText(app.translator.trans('core.forum.composer_discussion.body_placeholder'));\n attrs.submitLabel = attrs.submitLabel || app.translator.trans('core.forum.composer_discussion.submit_button');\n attrs.confirmExit = attrs.confirmExit || extractText(app.translator.trans('core.forum.composer_discussion.discard_confirmation'));\n attrs.titlePlaceholder = attrs.titlePlaceholder || extractText(app.translator.trans('core.forum.composer_discussion.title_placeholder'));\n attrs.className = 'ComposerBody--discussion';\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.composer.fields.title = this.composer.fields.title || Stream('');\n\n /**\n * The value of the title input.\n *\n * @type {Function}\n */\n this.title = this.composer.fields.title;\n }\n headerItems() {\n const items = super.headerItems();\n items.add('title', m(\"h3\", null, app.translator.trans('core.forum.composer_discussion.title')), 100);\n items.add('discussionTitle', m(\"h3\", null, m(\"input\", {\n className: \"FormControl\",\n bidi: this.title,\n placeholder: this.attrs.titlePlaceholder,\n disabled: !!this.attrs.disabled,\n onkeydown: this.onkeydown.bind(this)\n })));\n return items;\n }\n\n /**\n * Handle the title input's keydown event. When the return key is pressed,\n * move the focus to the start of the text editor.\n *\n * @param {KeyboardEvent} e\n */\n onkeydown(e) {\n if (e.which === 13) {\n // Return\n e.preventDefault();\n this.composer.editor.moveCursorTo(0);\n }\n e.redraw = false;\n }\n hasChanges() {\n return this.title() || this.composer.fields.content();\n }\n\n /**\n * Get the data to submit to the server when the discussion is saved.\n *\n * @return {Record}\n */\n data() {\n return {\n title: this.title(),\n content: this.composer.fields.content()\n };\n }\n onsubmit() {\n this.loading = true;\n const data = this.data();\n app.store.createRecord('discussions').save(data).then(discussion => {\n this.composer.hide();\n app.discussions.refresh();\n m.route.set(app.route.discussion(discussion));\n }, this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/DiscussionComposer', DiscussionComposer);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/DiscussionsUserPage.js b/framework/core/js/dist/forum/components/DiscussionsUserPage.js index 5be6acbd2a7..016b88a09e4 100644 --- a/framework/core/js/dist/forum/components/DiscussionsUserPage.js +++ b/framework/core/js/dist/forum/components/DiscussionsUserPage.js @@ -1,2 +1,52 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[799],{6466:(s,e,r)=>{r.r(e),r.d(e,{default:()=>u});var t=r(3390),a=r(8421),n=r(1654);class u extends t.Z{oninit(s){super.oninit(s),this.loadUser(m.route.param("username"))}show(s){super.show(s),this.state=new n.Z({filter:{author:s.username()},sort:"newest"}),this.state.refresh()}content(){return m("div",{className:"DiscussionsUserPage"},m(a.Z,{state:this.state}))}}flarum.reg.add("core","forum/components/DiscussionsUserPage",u)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/DiscussionsUserPage"],{ + +/***/ "./src/forum/components/DiscussionsUserPage.tsx": +/*!******************************************************!*\ + !*** ./src/forum/components/DiscussionsUserPage.tsx ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ DiscussionsUserPage) +/* harmony export */ }); +/* harmony import */ var _UserPage__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./UserPage */ "./src/forum/components/UserPage.tsx"); +/* harmony import */ var _DiscussionList__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./DiscussionList */ "./src/forum/components/DiscussionList.js"); +/* harmony import */ var _states_DiscussionListState__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../states/DiscussionListState */ "./src/forum/states/DiscussionListState.ts"); + + + +/** + * The `DiscussionsUserPage` component shows a discussion list inside of a user + * page. + */ +class DiscussionsUserPage extends _UserPage__WEBPACK_IMPORTED_MODULE_0__["default"] { + oninit(vnode) { + super.oninit(vnode); + this.loadUser(m.route.param('username')); + } + show(user) { + super.show(user); + this.state = new _states_DiscussionListState__WEBPACK_IMPORTED_MODULE_2__["default"]({ + filter: { + author: user.username() + }, + sort: 'newest' + }); + this.state.refresh(); + } + content() { + return m("div", { + className: "DiscussionsUserPage" + }, m(_DiscussionList__WEBPACK_IMPORTED_MODULE_1__["default"], { + state: this.state + })); + } +} +flarum.reg.add('core', 'forum/components/DiscussionsUserPage', DiscussionsUserPage); + +/***/ }) + +}]); //# sourceMappingURL=DiscussionsUserPage.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/DiscussionsUserPage.js.map b/framework/core/js/dist/forum/components/DiscussionsUserPage.js.map index 1bf260922ff..cfbdc5d553b 100644 --- a/framework/core/js/dist/forum/components/DiscussionsUserPage.js.map +++ b/framework/core/js/dist/forum/components/DiscussionsUserPage.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/DiscussionsUserPage.js","mappings":"yKAOe,MAAMA,UAA4B,IAC/CC,OAAOC,GACLC,MAAMF,OAAOC,GACbE,KAAKC,SAASC,EAAEC,MAAMC,MAAM,YAC9B,CACAC,KAAKC,GACHP,MAAMM,KAAKC,GACXN,KAAKO,MAAQ,IAAI,IAAoB,CACnCC,OAAQ,CACNC,OAAQH,EAAKI,YAEfC,KAAM,WAERX,KAAKO,MAAMK,SACb,CACAC,UACE,OAAOX,EAAE,MAAO,CACdY,UAAW,uBACVZ,EAAE,IAAgB,CACnBK,MAAOP,KAAKO,QAEhB,EAEFQ,OAAOC,IAAIC,IAAI,OAAQ,uCAAwCrB,E","sources":["webpack://@flarum/core/./src/forum/components/DiscussionsUserPage.tsx"],"sourcesContent":["import UserPage from './UserPage';\nimport DiscussionList from './DiscussionList';\nimport DiscussionListState from '../states/DiscussionListState';\n/**\n * The `DiscussionsUserPage` component shows a discussion list inside of a user\n * page.\n */\nexport default class DiscussionsUserPage extends UserPage {\n oninit(vnode) {\n super.oninit(vnode);\n this.loadUser(m.route.param('username'));\n }\n show(user) {\n super.show(user);\n this.state = new DiscussionListState({\n filter: {\n author: user.username()\n },\n sort: 'newest'\n });\n this.state.refresh();\n }\n content() {\n return m(\"div\", {\n className: \"DiscussionsUserPage\"\n }, m(DiscussionList, {\n state: this.state\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/DiscussionsUserPage', DiscussionsUserPage);"],"names":["DiscussionsUserPage","oninit","vnode","super","this","loadUser","m","route","param","show","user","state","filter","author","username","sort","refresh","content","className","flarum","reg","add"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/DiscussionsUserPage.js","mappings":";;;;;;;;;;;;;;;;AAAkC;AACY;AACkB;AAChE;AACA;AACA;AACA;AACe,kCAAkC,iDAAQ;AACzD;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,mEAAmB;AACxC;AACA;AACA,OAAO;AACP;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA,KAAK,IAAI,uDAAc;AACvB;AACA,KAAK;AACL;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/DiscussionsUserPage.tsx"],"sourcesContent":["import UserPage from './UserPage';\nimport DiscussionList from './DiscussionList';\nimport DiscussionListState from '../states/DiscussionListState';\n/**\n * The `DiscussionsUserPage` component shows a discussion list inside of a user\n * page.\n */\nexport default class DiscussionsUserPage extends UserPage {\n oninit(vnode) {\n super.oninit(vnode);\n this.loadUser(m.route.param('username'));\n }\n show(user) {\n super.show(user);\n this.state = new DiscussionListState({\n filter: {\n author: user.username()\n },\n sort: 'newest'\n });\n this.state.refresh();\n }\n content() {\n return m(\"div\", {\n className: \"DiscussionsUserPage\"\n }, m(DiscussionList, {\n state: this.state\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/DiscussionsUserPage', DiscussionsUserPage);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/EditPostComposer.js b/framework/core/js/dist/forum/components/EditPostComposer.js index 4fadd5b999a..30a32f69287 100644 --- a/framework/core/js/dist/forum/components/EditPostComposer.js +++ b/framework/core/js/dist/forum/components/EditPostComposer.js @@ -1,2 +1,293 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[293],{2140:(t,s,e)=>{e.d(s,{Z:()=>u});var o=e(2190),r=e(5226);class i extends o.Z{handler(){return this.attrs.when()||void 0}oncreate(t){super.oncreate(t),this.boundHandler=this.handler.bind(this),$(window).on("beforeunload",this.boundHandler)}onremove(t){super.onremove(t),$(window).off("beforeunload",this.boundHandler)}view(t){return m("[",null,t.children)}}flarum.reg.add("core","common/components/ConfirmDocumentUnload",i);var n=e(4944),a=e(1268),d=e(4041),c=e(3344),l=e(7323);class u extends o.Z{oninit(t){super.oninit(t),this.composer=this.attrs.composer,this.loading=!1,this.attrs.confirmExit&&this.composer.preventClosingWhen((()=>this.hasChanges()),this.attrs.confirmExit),this.composer.fields.content(this.attrs.originalContent||"")}view(){var t;return m(i,{when:this.hasChanges.bind(this)},m("div",{className:(0,c.Z)("ComposerBody",this.attrs.className)},m(l.Z,{user:this.attrs.user,className:"ComposerBody-avatar"}),m("div",{className:"ComposerBody-content"},m("ul",{className:"ComposerBody-header"},(0,a.Z)(this.headerItems().toArray())),m("div",{className:"ComposerBody-editor"},m(n.Z,{submitLabel:this.attrs.submitLabel,placeholder:this.attrs.placeholder,disabled:this.loading||this.attrs.disabled,composer:this.composer,preview:null==(t=this.jumpToPreview)?void 0:t.bind(this),onchange:this.composer.fields.content,onsubmit:this.onsubmit.bind(this),value:this.composer.fields.content()}))),m(r.Z,{display:"unset",containerClassName:(0,c.Z)("ComposerBody-loading",this.loading&&"active"),size:"large"})))}hasChanges(){const t=this.composer.fields.content();return t&&t!==this.attrs.originalContent}headerItems(){return new d.Z}onsubmit(){}loaded(){this.loading=!1,m.redraw()}}flarum.reg.add("core","forum/components/ComposerBody",u)},500:(t,s,e)=>{e.r(s),e.d(s,{default:()=>c});var o=e(6789),r=e(2140),i=e(8312),n=e(6597),a=e(9133);function d(t){o.Z.composer.isFullScreen()&&(o.Z.composer.minimize(),t.stopPropagation())}class c extends r.Z{static initAttrs(t){super.initAttrs(t),t.submitLabel=t.submitLabel||o.Z.translator.trans("core.forum.composer_edit.submit_button"),t.confirmExit=t.confirmExit||o.Z.translator.trans("core.forum.composer_edit.discard_confirmation"),t.originalContent=t.originalContent||t.post.content(),t.user=t.user||t.post.user(),t.post.editedContent=t.originalContent}headerItems(){const t=super.headerItems(),s=this.attrs.post;return t.add("title",m("h3",null,m(a.Z,{name:"fas fa-pencil-alt"})," ",m(n.Z,{href:o.Z.route.discussion(s.discussion(),s.number()),onclick:d},o.Z.translator.trans("core.forum.composer_edit.post_link",{number:s.number(),discussion:s.discussion().title()})))),t}jumpToPreview(t){d(t),m.route.set(o.Z.route.post(this.attrs.post))}data(){return{content:this.composer.fields.content()}}onsubmit(){const t=this.attrs.post.discussion();this.loading=!0;const s=this.data();this.attrs.post.save(s).then((s=>{if(o.Z.viewingDiscussion(t))o.Z.current.get("stream").goToNumber(s.number());else{const t=o.Z.alerts.show({type:"success",controls:[m(i.Z,{className:"Button Button--link",onclick:()=>{m.route.set(o.Z.route.post(s)),o.Z.alerts.dismiss(t)}},o.Z.translator.trans("core.forum.composer_edit.view_button"))]},o.Z.translator.trans("core.forum.composer_edit.edited_message"))}this.composer.hide()}),this.loaded.bind(this))}}flarum.reg.add("core","forum/components/EditPostComposer",c)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/EditPostComposer"],{ + +/***/ "./src/common/components/ConfirmDocumentUnload.js": +/*!********************************************************!*\ + !*** ./src/common/components/ConfirmDocumentUnload.js ***! + \********************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ConfirmDocumentUnload) +/* harmony export */ }); +/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ "./src/common/Component.ts"); + + +/** + * The `ConfirmDocumentUnload` component can be used to register a global + * event handler that prevents closing the browser window/tab based on the + * return value of a given callback prop. + * + * ### Attrs + * + * - `when` - a callback returning true when the browser should prompt for + * confirmation before closing the window/tab + */ +class ConfirmDocumentUnload extends _Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + handler() { + return this.attrs.when() || undefined; + } + oncreate(vnode) { + super.oncreate(vnode); + this.boundHandler = this.handler.bind(this); + $(window).on('beforeunload', this.boundHandler); + } + onremove(vnode) { + super.onremove(vnode); + $(window).off('beforeunload', this.boundHandler); + } + view(vnode) { + return m('[', null, vnode.children); + } +} +flarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload); + +/***/ }), + +/***/ "./src/forum/components/ComposerBody.js": +/*!**********************************************!*\ + !*** ./src/forum/components/ComposerBody.js ***! + \**********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ComposerBody) +/* harmony export */ }); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx"); +/* harmony import */ var _common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/ConfirmDocumentUnload */ "./src/common/components/ConfirmDocumentUnload.js"); +/* harmony import */ var _common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/TextEditor */ "./src/common/components/TextEditor.js"); +/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts"); +/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx"); + + + + + + + + + +/** + * The `ComposerBody` component handles the body, or the content, of the + * composer. Subclasses should implement the `onsubmit` method and override + * `headerTimes`. + * + * ### Attrs + * + * - `composer` + * - `originalContent` + * - `submitLabel` + * - `placeholder` + * - `user` + * - `confirmExit` + * - `disabled` + * + * @abstract + */ +class ComposerBody extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + oninit(vnode) { + super.oninit(vnode); + this.composer = this.attrs.composer; + + /** + * Whether or not the component is loading. + * + * @type {Boolean} + */ + this.loading = false; + + // Let the composer state know to ask for confirmation under certain + // circumstances, if the body supports / requires it and has a corresponding + // confirmation question to ask. + if (this.attrs.confirmExit) { + this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit); + } + this.composer.fields.content(this.attrs.originalContent || ''); + } + view() { + var _this$jumpToPreview; + return m(_common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__["default"], { + when: this.hasChanges.bind(this) + }, m("div", { + className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody', this.attrs.className) + }, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__["default"], { + user: this.attrs.user, + className: "ComposerBody-avatar" + }), m("div", { + className: "ComposerBody-content" + }, m("ul", { + className: "ComposerBody-header" + }, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__["default"])(this.headerItems().toArray())), m("div", { + className: "ComposerBody-editor" + }, m(_common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__["default"], { + submitLabel: this.attrs.submitLabel, + placeholder: this.attrs.placeholder, + disabled: this.loading || this.attrs.disabled, + composer: this.composer, + preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this), + onchange: this.composer.fields.content, + onsubmit: this.onsubmit.bind(this), + value: this.composer.fields.content() + }))), m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__["default"], { + display: "unset", + containerClassName: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody-loading', this.loading && 'active'), + size: "large" + }))); + } + + /** + * Check if there is any unsaved data. + * + * @return {boolean} + */ + hasChanges() { + const content = this.composer.fields.content(); + return content && content !== this.attrs.originalContent; + } + + /** + * Build an item list for the composer's header. + * + * @return {ItemList} + */ + headerItems() { + return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"](); + } + + /** + * Handle the submit event of the text editor. + * + * @abstract + */ + onsubmit() {} + + /** + * Stop loading. + */ + loaded() { + this.loading = false; + m.redraw(); + } +} +flarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody); + +/***/ }), + +/***/ "./src/forum/components/EditPostComposer.js": +/*!**************************************************!*\ + !*** ./src/forum/components/EditPostComposer.js ***! + \**************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ EditPostComposer) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _ComposerBody__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ComposerBody */ "./src/forum/components/ComposerBody.js"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_components_Link__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Link */ "./src/common/components/Link.js"); +/* harmony import */ var _common_components_Icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/components/Icon */ "./src/common/components/Icon.tsx"); + + + + + +function minimizeComposerIfFullScreen(e) { + if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.isFullScreen()) { + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.minimize(); + e.stopPropagation(); + } +} + +/** + * The `EditPostComposer` component displays the composer content for editing a + * post. It sets the initial content to the content of the post that is being + * edited, and adds a header control to indicate which post is being edited. + * + * ### Attrs + * + * - All of the attrs for ComposerBody + * - `post` + */ +class EditPostComposer extends _ComposerBody__WEBPACK_IMPORTED_MODULE_1__["default"] { + static initAttrs(attrs) { + super.initAttrs(attrs); + attrs.submitLabel = attrs.submitLabel || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.submit_button'); + attrs.confirmExit = attrs.confirmExit || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.discard_confirmation'); + attrs.originalContent = attrs.originalContent || attrs.post.content(); + attrs.user = attrs.user || attrs.post.user(); + attrs.post.editedContent = attrs.originalContent; + } + headerItems() { + const items = super.headerItems(); + const post = this.attrs.post; + items.add('title', m("h3", null, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_4__["default"], { + name: 'fas fa-pencil-alt' + }), ' ', m(_common_components_Link__WEBPACK_IMPORTED_MODULE_3__["default"], { + href: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(post.discussion(), post.number()), + onclick: minimizeComposerIfFullScreen + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.post_link', { + number: post.number(), + discussion: post.discussion().title() + })))); + return items; + } + + /** + * Jump to the preview when triggered by the text editor. + */ + jumpToPreview(e) { + minimizeComposerIfFullScreen(e); + m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.post(this.attrs.post)); + } + + /** + * Get the data to submit to the server when the post is saved. + * + * @return {Record} + */ + data() { + return { + content: this.composer.fields.content() + }; + } + onsubmit() { + const discussion = this.attrs.post.discussion(); + this.loading = true; + const data = this.data(); + this.attrs.post.save(data).then(post => { + // If we're currently viewing the discussion which this edit was made + // in, then we can scroll to the post. + if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].viewingDiscussion(discussion)) { + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].current.get('stream').goToNumber(post.number()); + } else { + // Otherwise, we'll create an alert message to inform the user that + // their edit has been made, containing a button which will + // transition to their edited post when clicked. + const alert = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.show({ + type: 'success', + controls: [m(_common_components_Button__WEBPACK_IMPORTED_MODULE_2__["default"], { + className: "Button Button--link", + onclick: () => { + m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.post(post)); + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.dismiss(alert); + } + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.view_button'))] + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_edit.edited_message')); + } + this.composer.hide(); + }, this.loaded.bind(this)); + } +} +flarum.reg.add('core', 'forum/components/EditPostComposer', EditPostComposer); + +/***/ }) + +}]); //# sourceMappingURL=EditPostComposer.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/EditPostComposer.js.map b/framework/core/js/dist/forum/components/EditPostComposer.js.map index dbeca2a0923..690cc0d59da 100644 --- a/framework/core/js/dist/forum/components/EditPostComposer.js.map +++ b/framework/core/js/dist/forum/components/EditPostComposer.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/EditPostComposer.js","mappings":"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","sources":["webpack://@flarum/core/./src/common/components/ConfirmDocumentUnload.js","webpack://@flarum/core/./src/forum/components/ComposerBody.js","webpack://@flarum/core/./src/forum/components/EditPostComposer.js"],"sourcesContent":["import Component from '../Component';\n\n/**\n * The `ConfirmDocumentUnload` component can be used to register a global\n * event handler that prevents closing the browser window/tab based on the\n * return value of a given callback prop.\n *\n * ### Attrs\n *\n * - `when` - a callback returning true when the browser should prompt for\n * confirmation before closing the window/tab\n */\nexport default class ConfirmDocumentUnload extends Component {\n handler() {\n return this.attrs.when() || undefined;\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.boundHandler = this.handler.bind(this);\n $(window).on('beforeunload', this.boundHandler);\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('beforeunload', this.boundHandler);\n }\n view(vnode) {\n return m('[', null, vnode.children);\n }\n}\nflarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);","import Component from '../../common/Component';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';\nimport TextEditor from '../../common/components/TextEditor';\nimport listItems from '../../common/helpers/listItems';\nimport ItemList from '../../common/utils/ItemList';\nimport classList from '../../common/utils/classList';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ComposerBody` component handles the body, or the content, of the\n * composer. Subclasses should implement the `onsubmit` method and override\n * `headerTimes`.\n *\n * ### Attrs\n *\n * - `composer`\n * - `originalContent`\n * - `submitLabel`\n * - `placeholder`\n * - `user`\n * - `confirmExit`\n * - `disabled`\n *\n * @abstract\n */\nexport default class ComposerBody extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.composer = this.attrs.composer;\n\n /**\n * Whether or not the component is loading.\n *\n * @type {Boolean}\n */\n this.loading = false;\n\n // Let the composer state know to ask for confirmation under certain\n // circumstances, if the body supports / requires it and has a corresponding\n // confirmation question to ask.\n if (this.attrs.confirmExit) {\n this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);\n }\n this.composer.fields.content(this.attrs.originalContent || '');\n }\n view() {\n var _this$jumpToPreview;\n return m(ConfirmDocumentUnload, {\n when: this.hasChanges.bind(this)\n }, m(\"div\", {\n className: classList('ComposerBody', this.attrs.className)\n }, m(Avatar, {\n user: this.attrs.user,\n className: \"ComposerBody-avatar\"\n }), m(\"div\", {\n className: \"ComposerBody-content\"\n }, m(\"ul\", {\n className: \"ComposerBody-header\"\n }, listItems(this.headerItems().toArray())), m(\"div\", {\n className: \"ComposerBody-editor\"\n }, m(TextEditor, {\n submitLabel: this.attrs.submitLabel,\n placeholder: this.attrs.placeholder,\n disabled: this.loading || this.attrs.disabled,\n composer: this.composer,\n preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),\n onchange: this.composer.fields.content,\n onsubmit: this.onsubmit.bind(this),\n value: this.composer.fields.content()\n }))), m(LoadingIndicator, {\n display: \"unset\",\n containerClassName: classList('ComposerBody-loading', this.loading && 'active'),\n size: \"large\"\n })));\n }\n\n /**\n * Check if there is any unsaved data.\n *\n * @return {boolean}\n */\n hasChanges() {\n const content = this.composer.fields.content();\n return content && content !== this.attrs.originalContent;\n }\n\n /**\n * Build an item list for the composer's header.\n *\n * @return {ItemList}\n */\n headerItems() {\n return new ItemList();\n }\n\n /**\n * Handle the submit event of the text editor.\n *\n * @abstract\n */\n onsubmit() {}\n\n /**\n * Stop loading.\n */\n loaded() {\n this.loading = false;\n m.redraw();\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);","import app from '../../forum/app';\nimport ComposerBody from './ComposerBody';\nimport Button from '../../common/components/Button';\nimport Link from '../../common/components/Link';\nimport Icon from '../../common/components/Icon';\nfunction minimizeComposerIfFullScreen(e) {\n if (app.composer.isFullScreen()) {\n app.composer.minimize();\n e.stopPropagation();\n }\n}\n\n/**\n * The `EditPostComposer` component displays the composer content for editing a\n * post. It sets the initial content to the content of the post that is being\n * edited, and adds a header control to indicate which post is being edited.\n *\n * ### Attrs\n *\n * - All of the attrs for ComposerBody\n * - `post`\n */\nexport default class EditPostComposer extends ComposerBody {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.submitLabel = attrs.submitLabel || app.translator.trans('core.forum.composer_edit.submit_button');\n attrs.confirmExit = attrs.confirmExit || app.translator.trans('core.forum.composer_edit.discard_confirmation');\n attrs.originalContent = attrs.originalContent || attrs.post.content();\n attrs.user = attrs.user || attrs.post.user();\n attrs.post.editedContent = attrs.originalContent;\n }\n headerItems() {\n const items = super.headerItems();\n const post = this.attrs.post;\n items.add('title', m(\"h3\", null, m(Icon, {\n name: 'fas fa-pencil-alt'\n }), ' ', m(Link, {\n href: app.route.discussion(post.discussion(), post.number()),\n onclick: minimizeComposerIfFullScreen\n }, app.translator.trans('core.forum.composer_edit.post_link', {\n number: post.number(),\n discussion: post.discussion().title()\n }))));\n return items;\n }\n\n /**\n * Jump to the preview when triggered by the text editor.\n */\n jumpToPreview(e) {\n minimizeComposerIfFullScreen(e);\n m.route.set(app.route.post(this.attrs.post));\n }\n\n /**\n * Get the data to submit to the server when the post is saved.\n *\n * @return {Record}\n */\n data() {\n return {\n content: this.composer.fields.content()\n };\n }\n onsubmit() {\n const discussion = this.attrs.post.discussion();\n this.loading = true;\n const data = this.data();\n this.attrs.post.save(data).then(post => {\n // If we're currently viewing the discussion which this edit was made\n // in, then we can scroll to the post.\n if (app.viewingDiscussion(discussion)) {\n app.current.get('stream').goToNumber(post.number());\n } else {\n // Otherwise, we'll create an alert message to inform the user that\n // their edit has been made, containing a button which will\n // transition to their edited post when clicked.\n const alert = app.alerts.show({\n type: 'success',\n controls: [m(Button, {\n className: \"Button Button--link\",\n onclick: () => {\n m.route.set(app.route.post(post));\n app.alerts.dismiss(alert);\n }\n }, app.translator.trans('core.forum.composer_edit.view_button'))]\n }, app.translator.trans('core.forum.composer_edit.edited_message'));\n }\n this.composer.hide();\n }, this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/EditPostComposer', EditPostComposer);"],"names":["ConfirmDocumentUnload","Component","handler","this","attrs","when","undefined","oncreate","vnode","super","boundHandler","bind","$","window","on","onremove","off","view","m","children","flarum","reg","add","ComposerBody","oninit","composer","loading","confirmExit","preventClosingWhen","hasChanges","fields","content","originalContent","_this$jumpToPreview","className","classList","Avatar","user","listItems","headerItems","toArray","TextEditor","submitLabel","placeholder","disabled","preview","jumpToPreview","onchange","onsubmit","value","LoadingIndicator","display","containerClassName","size","ItemList","loaded","redraw","minimizeComposerIfFullScreen","e","stopPropagation","EditPostComposer","static","initAttrs","post","editedContent","items","name","href","discussion","number","onclick","title","route","set","data","save","then","goToNumber","alert","type","controls","hide"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/EditPostComposer.js","mappings":";;;;;;;;;;;;;;AAAqC;;AAErC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,oCAAoC,kDAAS;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC7B+C;AACyB;AACU;AACtB;AACL;AACJ;AACE;AACD;;AAEpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,2BAA2B,yDAAS;AACnD;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,gFAAqB;AAClC;AACA,KAAK;AACL,iBAAiB,mEAAS;AAC1B,KAAK,IAAI,iEAAM;AACf;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK,EAAE,qEAAS;AAChB;AACA,KAAK,IAAI,qEAAU;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,OAAO,2EAAgB;AAC5B;AACA,0BAA0B,mEAAS;AACnC;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA,eAAe,8DAAQ;AACvB;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC/GkC;AACQ;AACU;AACJ;AACA;AAChD;AACA,MAAM,wEAAyB;AAC/B,IAAI,oEAAqB;AACzB;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,+BAA+B,qDAAY;AAC1D;AACA;AACA,6CAA6C,mEAAoB;AACjE,6CAA6C,mEAAoB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uCAAuC,+DAAI;AAC3C;AACA,KAAK,UAAU,+DAAI;AACnB,YAAY,mEAAoB;AAChC;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,gBAAgB,6DAAc;AAC9B;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU,oEAAqB;AAC/B,QAAQ,8DAAe;AACvB,QAAQ;AACR;AACA;AACA;AACA,sBAAsB,8DAAe;AACrC;AACA,uBAAuB,iEAAM;AAC7B;AACA;AACA,0BAA0B,6DAAc;AACxC,cAAc,iEAAkB;AAChC;AACA,WAAW,EAAE,mEAAoB;AACjC,SAAS,EAAE,mEAAoB;AAC/B;AACA;AACA,KAAK;AACL;AACA;AACA","sources":["webpack://@flarum/core/./src/common/components/ConfirmDocumentUnload.js","webpack://@flarum/core/./src/forum/components/ComposerBody.js","webpack://@flarum/core/./src/forum/components/EditPostComposer.js"],"sourcesContent":["import Component from '../Component';\n\n/**\n * The `ConfirmDocumentUnload` component can be used to register a global\n * event handler that prevents closing the browser window/tab based on the\n * return value of a given callback prop.\n *\n * ### Attrs\n *\n * - `when` - a callback returning true when the browser should prompt for\n * confirmation before closing the window/tab\n */\nexport default class ConfirmDocumentUnload extends Component {\n handler() {\n return this.attrs.when() || undefined;\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.boundHandler = this.handler.bind(this);\n $(window).on('beforeunload', this.boundHandler);\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('beforeunload', this.boundHandler);\n }\n view(vnode) {\n return m('[', null, vnode.children);\n }\n}\nflarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);","import Component from '../../common/Component';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';\nimport TextEditor from '../../common/components/TextEditor';\nimport listItems from '../../common/helpers/listItems';\nimport ItemList from '../../common/utils/ItemList';\nimport classList from '../../common/utils/classList';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ComposerBody` component handles the body, or the content, of the\n * composer. Subclasses should implement the `onsubmit` method and override\n * `headerTimes`.\n *\n * ### Attrs\n *\n * - `composer`\n * - `originalContent`\n * - `submitLabel`\n * - `placeholder`\n * - `user`\n * - `confirmExit`\n * - `disabled`\n *\n * @abstract\n */\nexport default class ComposerBody extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.composer = this.attrs.composer;\n\n /**\n * Whether or not the component is loading.\n *\n * @type {Boolean}\n */\n this.loading = false;\n\n // Let the composer state know to ask for confirmation under certain\n // circumstances, if the body supports / requires it and has a corresponding\n // confirmation question to ask.\n if (this.attrs.confirmExit) {\n this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);\n }\n this.composer.fields.content(this.attrs.originalContent || '');\n }\n view() {\n var _this$jumpToPreview;\n return m(ConfirmDocumentUnload, {\n when: this.hasChanges.bind(this)\n }, m(\"div\", {\n className: classList('ComposerBody', this.attrs.className)\n }, m(Avatar, {\n user: this.attrs.user,\n className: \"ComposerBody-avatar\"\n }), m(\"div\", {\n className: \"ComposerBody-content\"\n }, m(\"ul\", {\n className: \"ComposerBody-header\"\n }, listItems(this.headerItems().toArray())), m(\"div\", {\n className: \"ComposerBody-editor\"\n }, m(TextEditor, {\n submitLabel: this.attrs.submitLabel,\n placeholder: this.attrs.placeholder,\n disabled: this.loading || this.attrs.disabled,\n composer: this.composer,\n preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),\n onchange: this.composer.fields.content,\n onsubmit: this.onsubmit.bind(this),\n value: this.composer.fields.content()\n }))), m(LoadingIndicator, {\n display: \"unset\",\n containerClassName: classList('ComposerBody-loading', this.loading && 'active'),\n size: \"large\"\n })));\n }\n\n /**\n * Check if there is any unsaved data.\n *\n * @return {boolean}\n */\n hasChanges() {\n const content = this.composer.fields.content();\n return content && content !== this.attrs.originalContent;\n }\n\n /**\n * Build an item list for the composer's header.\n *\n * @return {ItemList}\n */\n headerItems() {\n return new ItemList();\n }\n\n /**\n * Handle the submit event of the text editor.\n *\n * @abstract\n */\n onsubmit() {}\n\n /**\n * Stop loading.\n */\n loaded() {\n this.loading = false;\n m.redraw();\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);","import app from '../../forum/app';\nimport ComposerBody from './ComposerBody';\nimport Button from '../../common/components/Button';\nimport Link from '../../common/components/Link';\nimport Icon from '../../common/components/Icon';\nfunction minimizeComposerIfFullScreen(e) {\n if (app.composer.isFullScreen()) {\n app.composer.minimize();\n e.stopPropagation();\n }\n}\n\n/**\n * The `EditPostComposer` component displays the composer content for editing a\n * post. It sets the initial content to the content of the post that is being\n * edited, and adds a header control to indicate which post is being edited.\n *\n * ### Attrs\n *\n * - All of the attrs for ComposerBody\n * - `post`\n */\nexport default class EditPostComposer extends ComposerBody {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.submitLabel = attrs.submitLabel || app.translator.trans('core.forum.composer_edit.submit_button');\n attrs.confirmExit = attrs.confirmExit || app.translator.trans('core.forum.composer_edit.discard_confirmation');\n attrs.originalContent = attrs.originalContent || attrs.post.content();\n attrs.user = attrs.user || attrs.post.user();\n attrs.post.editedContent = attrs.originalContent;\n }\n headerItems() {\n const items = super.headerItems();\n const post = this.attrs.post;\n items.add('title', m(\"h3\", null, m(Icon, {\n name: 'fas fa-pencil-alt'\n }), ' ', m(Link, {\n href: app.route.discussion(post.discussion(), post.number()),\n onclick: minimizeComposerIfFullScreen\n }, app.translator.trans('core.forum.composer_edit.post_link', {\n number: post.number(),\n discussion: post.discussion().title()\n }))));\n return items;\n }\n\n /**\n * Jump to the preview when triggered by the text editor.\n */\n jumpToPreview(e) {\n minimizeComposerIfFullScreen(e);\n m.route.set(app.route.post(this.attrs.post));\n }\n\n /**\n * Get the data to submit to the server when the post is saved.\n *\n * @return {Record}\n */\n data() {\n return {\n content: this.composer.fields.content()\n };\n }\n onsubmit() {\n const discussion = this.attrs.post.discussion();\n this.loading = true;\n const data = this.data();\n this.attrs.post.save(data).then(post => {\n // If we're currently viewing the discussion which this edit was made\n // in, then we can scroll to the post.\n if (app.viewingDiscussion(discussion)) {\n app.current.get('stream').goToNumber(post.number());\n } else {\n // Otherwise, we'll create an alert message to inform the user that\n // their edit has been made, containing a button which will\n // transition to their edited post when clicked.\n const alert = app.alerts.show({\n type: 'success',\n controls: [m(Button, {\n className: \"Button Button--link\",\n onclick: () => {\n m.route.set(app.route.post(post));\n app.alerts.dismiss(alert);\n }\n }, app.translator.trans('core.forum.composer_edit.view_button'))]\n }, app.translator.trans('core.forum.composer_edit.edited_message'));\n }\n this.composer.hide();\n }, this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/EditPostComposer', EditPostComposer);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/ForgotPasswordModal.js b/framework/core/js/dist/forum/components/ForgotPasswordModal.js index 71c549cf42f..de6e79956fb 100644 --- a/framework/core/js/dist/forum/components/ForgotPasswordModal.js +++ b/framework/core/js/dist/forum/components/ForgotPasswordModal.js @@ -1,2 +1,129 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[502],{1839:(r,t,s)=>{s.r(t),s.d(t,{default:()=>c});var o=s(7905),a=s(6789),e=s(899),l=s(8312),i=s(1552),n=s(6458),d=s(4041),u=s(6352);class c extends e.Z{constructor(){super(...arguments),(0,o.Z)(this,"email",void 0),(0,o.Z)(this,"success",!1)}oninit(r){super.oninit(r),this.email=(0,n.Z)(this.attrs.email||"")}className(){return"ForgotPasswordModal Modal--small"}title(){return a.Z.translator.trans("core.forum.forgot_password.title")}content(){return this.success?m("div",{className:"Modal-body"},m(u.Z,{className:"Form--centered"},m("p",{className:"helpText"},a.Z.translator.trans("core.forum.forgot_password.email_sent_message")),m("div",{className:"Form-group Form-controls"},m(l.Z,{className:"Button Button--primary Button--block",onclick:this.hide.bind(this)},a.Z.translator.trans("core.forum.forgot_password.dismiss_button"))))):m("div",{className:"Modal-body"},m(u.Z,{className:"Form--centered",description:a.Z.translator.trans("core.forum.forgot_password.text")},this.fields().toArray()))}fields(){const r=new d.Z,t=(0,i.Z)(a.Z.translator.trans("core.forum.forgot_password.email_placeholder"));return r.add("email",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"email",type:"email",placeholder:t,"aria-label":t,bidi:this.email,disabled:this.loading})),50),r.add("submit",m("div",{className:"Form-group Form-controls"},m(l.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},a.Z.translator.trans("core.forum.forgot_password.submit_button"))),-10),r}onsubmit(r){r.preventDefault(),this.loading=!0,a.Z.request({method:"POST",url:a.Z.forum.attribute("apiUrl")+"/forgot",body:this.requestParams(),errorHandler:this.onerror.bind(this)}).then((()=>{this.success=!0,this.alertAttrs=null})).catch((()=>{})).then(this.loaded.bind(this))}requestParams(){return{email:this.email()}}onerror(r){404===r.status&&r.alert&&(r.alert.content=a.Z.translator.trans("core.forum.forgot_password.not_found_message")),super.onerror(r)}}flarum.reg.add("core","forum/components/ForgotPasswordModal",c)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/ForgotPasswordModal"],{ + +/***/ "./src/forum/components/ForgotPasswordModal.tsx": +/*!******************************************************!*\ + !*** ./src/forum/components/ForgotPasswordModal.tsx ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ForgotPasswordModal) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_components_Form__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Form */ "./src/common/components/Form.tsx"); + + + + + + + + +/** + * The `ForgotPasswordModal` component displays a modal which allows the user to + * enter their email address and request a link to reset their password. + */ +class ForgotPasswordModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + /** + * The value of the email input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "email", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "success", false); + } + oninit(vnode) { + super.oninit(vnode); + this.email = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_5__["default"])(this.attrs.email || ''); + } + className() { + return 'ForgotPasswordModal Modal--small'; + } + title() { + return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.title'); + } + content() { + if (this.success) { + return m("div", { + className: "Modal-body" + }, m(_common_components_Form__WEBPACK_IMPORTED_MODULE_7__["default"], { + className: "Form--centered" + }, m("p", { + className: "helpText" + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.email_sent_message')), m("div", { + className: "Form-group Form-controls" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary Button--block", + onclick: this.hide.bind(this) + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.dismiss_button'))))); + } + return m("div", { + className: "Modal-body" + }, m(_common_components_Form__WEBPACK_IMPORTED_MODULE_7__["default"], { + className: "Form--centered", + description: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.text') + }, this.fields().toArray())); + } + fields() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__["default"](); + const emailLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.email_placeholder')); + items.add('email', m("div", { + className: "Form-group" + }, m("input", { + className: "FormControl", + name: "email", + type: "email", + placeholder: emailLabel, + "aria-label": emailLabel, + bidi: this.email, + disabled: this.loading + })), 50); + items.add('submit', m("div", { + className: "Form-group Form-controls" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary Button--block", + type: "submit", + loading: this.loading + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.submit_button'))), -10); + return items; + } + onsubmit(e) { + e.preventDefault(); + this.loading = true; + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].request({ + method: 'POST', + url: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('apiUrl') + '/forgot', + body: this.requestParams(), + errorHandler: this.onerror.bind(this) + }).then(() => { + this.success = true; + this.alertAttrs = null; + }).catch(() => {}).then(this.loaded.bind(this)); + } + requestParams() { + const data = { + email: this.email() + }; + return data; + } + onerror(error) { + if (error.status === 404 && error.alert) { + error.alert.content = _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.forgot_password.not_found_message'); + } + super.onerror(error); + } +} +flarum.reg.add('core', 'forum/components/ForgotPasswordModal', ForgotPasswordModal); + +/***/ }) + +}]); //# sourceMappingURL=ForgotPasswordModal.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/ForgotPasswordModal.js.map b/framework/core/js/dist/forum/components/ForgotPasswordModal.js.map index 86f73e30af1..490eb5e9520 100644 --- a/framework/core/js/dist/forum/components/ForgotPasswordModal.js.map +++ b/framework/core/js/dist/forum/components/ForgotPasswordModal.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/ForgotPasswordModal.js","mappings":"0NAYe,MAAMA,UAA4B,IAC/CC,cACEC,SAASC,YAIT,OAAgBC,KAAM,aAAS,IAC/B,OAAgBA,KAAM,WAAW,EACnC,CACAC,OAAOC,GACLJ,MAAMG,OAAOC,GACbF,KAAKG,OAAQ,OAAOH,KAAKI,MAAMD,OAAS,GAC1C,CACAE,YACE,MAAO,kCACT,CACAC,QACE,OAAO,qBAAqB,mCAC9B,CACAC,UACE,OAAIP,KAAKQ,QACAC,EAAE,MAAO,CACdJ,UAAW,cACVI,EAAE,IAAM,CACTJ,UAAW,kBACVI,EAAE,IAAK,CACRJ,UAAW,YACV,qBAAqB,kDAAmDI,EAAE,MAAO,CAClFJ,UAAW,4BACVI,EAAE,IAAQ,CACXJ,UAAW,uCACXK,QAASV,KAAKW,KAAKC,KAAKZ,OACvB,qBAAqB,iDAEnBS,EAAE,MAAO,CACdJ,UAAW,cACVI,EAAE,IAAM,CACTJ,UAAW,iBACXQ,YAAa,qBAAqB,oCACjCb,KAAKc,SAASC,WACnB,CACAD,SACE,MAAME,EAAQ,IAAI,IACZC,GAAa,OAAY,qBAAqB,iDAmBpD,OAlBAD,EAAME,IAAI,QAAST,EAAE,MAAO,CAC1BJ,UAAW,cACVI,EAAE,QAAS,CACZJ,UAAW,cACXc,KAAM,QACNC,KAAM,QACNC,YAAaJ,EACb,aAAcA,EACdK,KAAMtB,KAAKG,MACXoB,SAAUvB,KAAKwB,WACZ,IACLR,EAAME,IAAI,SAAUT,EAAE,MAAO,CAC3BJ,UAAW,4BACVI,EAAE,IAAQ,CACXJ,UAAW,uCACXe,KAAM,SACNI,QAASxB,KAAKwB,SACb,qBAAqB,+CAAgD,IACjER,CACT,CACAS,SAASC,GACPA,EAAEC,iBACF3B,KAAKwB,SAAU,EACf,YAAY,CACVI,OAAQ,OACRC,IAAK,oBAAoB,UAAY,UACrCC,KAAM9B,KAAK+B,gBACXC,aAAchC,KAAKiC,QAAQrB,KAAKZ,QAC/BkC,MAAK,KACNlC,KAAKQ,SAAU,EACfR,KAAKmC,WAAa,IAAI,IACrBC,OAAM,SAAUF,KAAKlC,KAAKqC,OAAOzB,KAAKZ,MAC3C,CACA+B,gBAIE,MAHa,CACX5B,MAAOH,KAAKG,QAGhB,CACA8B,QAAQK,GACe,MAAjBA,EAAMC,QAAkBD,EAAME,QAChCF,EAAME,MAAMjC,QAAU,qBAAqB,iDAE7CT,MAAMmC,QAAQK,EAChB,EAEFG,OAAOC,IAAIxB,IAAI,OAAQ,uCAAwCtB,E","sources":["webpack://@flarum/core/./src/forum/components/ForgotPasswordModal.tsx"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport extractText from '../../common/utils/extractText';\nimport Stream from '../../common/utils/Stream';\nimport ItemList from '../../common/utils/ItemList';\nimport Form from '../../common/components/Form';\n/**\n * The `ForgotPasswordModal` component displays a modal which allows the user to\n * enter their email address and request a link to reset their password.\n */\nexport default class ForgotPasswordModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the email input.\n */\n _defineProperty(this, \"email\", void 0);\n _defineProperty(this, \"success\", false);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.email = Stream(this.attrs.email || '');\n }\n className() {\n return 'ForgotPasswordModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.forgot_password.title');\n }\n content() {\n if (this.success) {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, m(\"p\", {\n className: \"helpText\"\n }, app.translator.trans('core.forum.forgot_password.email_sent_message')), m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n onclick: this.hide.bind(this)\n }, app.translator.trans('core.forum.forgot_password.dismiss_button')))));\n }\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\",\n description: app.translator.trans('core.forum.forgot_password.text')\n }, this.fields().toArray()));\n }\n fields() {\n const items = new ItemList();\n const emailLabel = extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'));\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"email\",\n type: \"email\",\n placeholder: emailLabel,\n \"aria-label\": emailLabel,\n bidi: this.email,\n disabled: this.loading\n })), 50);\n items.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.forgot_password.submit_button'))), -10);\n return items;\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n app.request({\n method: 'POST',\n url: app.forum.attribute('apiUrl') + '/forgot',\n body: this.requestParams(),\n errorHandler: this.onerror.bind(this)\n }).then(() => {\n this.success = true;\n this.alertAttrs = null;\n }).catch(() => {}).then(this.loaded.bind(this));\n }\n requestParams() {\n const data = {\n email: this.email()\n };\n return data;\n }\n onerror(error) {\n if (error.status === 404 && error.alert) {\n error.alert.content = app.translator.trans('core.forum.forgot_password.not_found_message');\n }\n super.onerror(error);\n }\n}\nflarum.reg.add('core', 'forum/components/ForgotPasswordModal', ForgotPasswordModal);"],"names":["ForgotPasswordModal","constructor","super","arguments","this","oninit","vnode","email","attrs","className","title","content","success","m","onclick","hide","bind","description","fields","toArray","items","emailLabel","add","name","type","placeholder","bidi","disabled","loading","onsubmit","e","preventDefault","method","url","body","requestParams","errorHandler","onerror","then","alertAttrs","catch","loaded","error","status","alert","flarum","reg"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/ForgotPasswordModal.js","mappings":";;;;;;;;;;;;;;;;;;;;;AAAwE;AACtC;AACwB;AACN;AACK;AACV;AACI;AACH;AAChD;AACA;AACA;AACA;AACe,kCAAkC,oEAAS;AAC1D;AACA;AACA;AACA;AACA;AACA,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB;AACA;AACA;AACA,iBAAiB,gEAAM;AACvB;AACA;AACA;AACA;AACA;AACA,WAAW,mEAAoB;AAC/B;AACA;AACA;AACA;AACA;AACA,OAAO,IAAI,+DAAI;AACf;AACA,OAAO;AACP;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA,OAAO,IAAI,iEAAM;AACjB;AACA;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,mBAAmB,mEAAoB;AACvC,KAAK;AACL;AACA;AACA,sBAAsB,8DAAQ;AAC9B,uBAAuB,qEAAW,CAAC,mEAAoB;AACvD;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK,IAAI,iEAAM;AACf;AACA;AACA;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA;AACA;AACA;AACA,IAAI,0DAAW;AACf;AACA,WAAW,kEAAmB;AAC9B;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,mEAAoB;AAChD;AACA;AACA;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/ForgotPasswordModal.tsx"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport extractText from '../../common/utils/extractText';\nimport Stream from '../../common/utils/Stream';\nimport ItemList from '../../common/utils/ItemList';\nimport Form from '../../common/components/Form';\n/**\n * The `ForgotPasswordModal` component displays a modal which allows the user to\n * enter their email address and request a link to reset their password.\n */\nexport default class ForgotPasswordModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the email input.\n */\n _defineProperty(this, \"email\", void 0);\n _defineProperty(this, \"success\", false);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.email = Stream(this.attrs.email || '');\n }\n className() {\n return 'ForgotPasswordModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.forgot_password.title');\n }\n content() {\n if (this.success) {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, m(\"p\", {\n className: \"helpText\"\n }, app.translator.trans('core.forum.forgot_password.email_sent_message')), m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n onclick: this.hide.bind(this)\n }, app.translator.trans('core.forum.forgot_password.dismiss_button')))));\n }\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\",\n description: app.translator.trans('core.forum.forgot_password.text')\n }, this.fields().toArray()));\n }\n fields() {\n const items = new ItemList();\n const emailLabel = extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'));\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"email\",\n type: \"email\",\n placeholder: emailLabel,\n \"aria-label\": emailLabel,\n bidi: this.email,\n disabled: this.loading\n })), 50);\n items.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.forgot_password.submit_button'))), -10);\n return items;\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n app.request({\n method: 'POST',\n url: app.forum.attribute('apiUrl') + '/forgot',\n body: this.requestParams(),\n errorHandler: this.onerror.bind(this)\n }).then(() => {\n this.success = true;\n this.alertAttrs = null;\n }).catch(() => {}).then(this.loaded.bind(this));\n }\n requestParams() {\n const data = {\n email: this.email()\n };\n return data;\n }\n onerror(error) {\n if (error.status === 404 && error.alert) {\n error.alert.content = app.translator.trans('core.forum.forgot_password.not_found_message');\n }\n super.onerror(error);\n }\n}\nflarum.reg.add('core', 'forum/components/ForgotPasswordModal', ForgotPasswordModal);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/LogInModal.js b/framework/core/js/dist/forum/components/LogInModal.js index 02d058f9448..0c5ce9eae26 100644 --- a/framework/core/js/dist/forum/components/LogInModal.js +++ b/framework/core/js/dist/forum/components/LogInModal.js @@ -1,2 +1,222 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[460],{5049:(o,r,t)=>{t.r(r),t.d(r,{default:()=>u});var s=t(7905),e=t(6789),i=t(899),a=t(8312),n=t(6403),l=t(1552),d=t(4041),c=t(6458);class u extends i.Z{constructor(){super(...arguments),(0,s.Z)(this,"identification",void 0),(0,s.Z)(this,"password",void 0),(0,s.Z)(this,"remember",void 0)}oninit(o){super.oninit(o),this.identification=(0,c.Z)(this.attrs.identification||""),this.password=(0,c.Z)(this.attrs.password||""),this.remember=(0,c.Z)(!!this.attrs.remember)}className(){return"LogInModal Modal--small"}title(){return e.Z.translator.trans("core.forum.log_in.title")}content(){return[m("div",{className:"Modal-body"},this.body()),m("div",{className:"Modal-footer"},this.footer())]}body(){return[m(n.Z,null),m("div",{className:"Form Form--centered"},this.fields().toArray())]}fields(){const o=new d.Z,r=(0,l.Z)(e.Z.translator.trans("core.forum.log_in.username_or_email_placeholder")),t=(0,l.Z)(e.Z.translator.trans("core.forum.log_in.password_placeholder"));return o.add("identification",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"identification",type:"text",placeholder:r,"aria-label":r,bidi:this.identification,disabled:this.loading})),30),o.add("password",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"password",type:"password",autocomplete:"current-password",placeholder:t,"aria-label":t,bidi:this.password,disabled:this.loading})),20),o.add("remember",m("div",{className:"Form-group"},m("div",null,m("label",{className:"checkbox"},m("input",{type:"checkbox",bidi:this.remember,disabled:this.loading}),e.Z.translator.trans("core.forum.log_in.remember_me_label")))),10),o.add("submit",m("div",{className:"Form-group"},m(a.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},e.Z.translator.trans("core.forum.log_in.submit_button"))),-10),o}footer(){return m("[",null,m("p",{className:"LogInModal-forgotPassword"},m("a",{onclick:this.forgotPassword.bind(this)},e.Z.translator.trans("core.forum.log_in.forgot_password_link"))),e.Z.forum.attribute("allowSignUp")&&m("p",{className:"LogInModal-signUp"},e.Z.translator.trans("core.forum.log_in.sign_up_text",{a:m("a",{onclick:this.signUp.bind(this)})})))}forgotPassword(){const o=this.identification(),r=o.includes("@")?{email:o}:void 0;e.Z.modal.show((()=>t.e(502).then(t.bind(t,1839))),r)}signUp(){const o=this.identification(),r={[o.includes("@")?"email":"username"]:o};e.Z.modal.show((()=>t.e(395).then(t.bind(t,8686))),r)}onready(){this.$("[name="+(this.identification()?"password":"identification")+"]").trigger("select")}onsubmit(o){o.preventDefault(),this.loading=!0,e.Z.session.login(this.loginParams(),{errorHandler:this.onerror.bind(this)}).then((()=>window.location.reload()),this.loaded.bind(this))}loginParams(){return{identification:this.identification(),password:this.password(),remember:this.remember()}}onerror(o){401===o.status&&o.alert&&(o.alert.content=e.Z.translator.trans("core.forum.log_in.invalid_login_message"),this.password("")),super.onerror(o)}}flarum.reg.add("core","forum/components/LogInModal",u),flarum.reg.addChunkModule("502","1839","core","forum/components/ForgotPasswordModal"),flarum.reg.addChunkModule("395","8686","core","forum/components/SignUpModal")},6403:(o,r,t)=>{t.d(r,{Z:()=>i});var s=t(2190),e=t(4041);class i extends s.Z{view(){return m("div",{className:"LogInButtons"},this.items().toArray())}items(){return new e.Z}}flarum.reg.add("core","forum/components/LogInButtons",i)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/LogInModal"],{ + +/***/ "./src/forum/components/LogInModal.tsx": +/*!*********************************************!*\ + !*** ./src/forum/components/LogInModal.tsx ***! + \*********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ LogInModal) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _LogInButtons__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./LogInButtons */ "./src/forum/components/LogInButtons.js"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts"); + + + + + + + + +class LogInModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + /** + * The value of the identification input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "identification", void 0); + /** + * The value of the password input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "password", void 0); + /** + * The value of the remember me input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "remember", void 0); + } + oninit(vnode) { + super.oninit(vnode); + this.identification = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.identification || ''); + this.password = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.password || ''); + this.remember = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(!!this.attrs.remember); + } + className() { + return 'LogInModal Modal--small'; + } + title() { + return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.title'); + } + content() { + return [m("div", { + className: "Modal-body" + }, this.body()), m("div", { + className: "Modal-footer" + }, this.footer())]; + } + body() { + return [m(_LogInButtons__WEBPACK_IMPORTED_MODULE_4__["default"], null), m("div", { + className: "Form Form--centered" + }, this.fields().toArray())]; + } + fields() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__["default"](); + const identificationLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.username_or_email_placeholder')); + const passwordLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.password_placeholder')); + items.add('identification', m("div", { + className: "Form-group" + }, m("input", { + className: "FormControl", + name: "identification", + type: "text", + placeholder: identificationLabel, + "aria-label": identificationLabel, + bidi: this.identification, + disabled: this.loading + })), 30); + items.add('password', m("div", { + className: "Form-group" + }, m("input", { + className: "FormControl", + name: "password", + type: "password", + autocomplete: "current-password", + placeholder: passwordLabel, + "aria-label": passwordLabel, + bidi: this.password, + disabled: this.loading + })), 20); + items.add('remember', m("div", { + className: "Form-group" + }, m("div", null, m("label", { + className: "checkbox" + }, m("input", { + type: "checkbox", + bidi: this.remember, + disabled: this.loading + }), _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.remember_me_label')))), 10); + items.add('submit', m("div", { + className: "Form-group" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary Button--block", + type: "submit", + loading: this.loading + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.submit_button'))), -10); + return items; + } + footer() { + return m('[', null, m("p", { + className: "LogInModal-forgotPassword" + }, m("a", { + onclick: this.forgotPassword.bind(this) + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.forgot_password_link'))), _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('allowSignUp') && m("p", { + className: "LogInModal-signUp" + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.sign_up_text', { + a: m("a", { + onclick: this.signUp.bind(this) + }) + }))); + } + + /** + * Open the forgot password modal, prefilling it with an email if the user has + * entered one. + */ + forgotPassword() { + const email = this.identification(); + const attrs = email.includes('@') ? { + email + } : undefined; + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(() => __webpack_require__.e(/*! import() | forum/components/ForgotPasswordModal */ "forum/components/ForgotPasswordModal").then(__webpack_require__.bind(__webpack_require__, /*! ./ForgotPasswordModal */ "./src/forum/components/ForgotPasswordModal.tsx")), attrs); + } + + /** + * Open the sign up modal, prefilling it with an email/username/password if + * the user has entered one. + */ + signUp() { + const identification = this.identification(); + const attrs = { + [identification.includes('@') ? 'email' : 'username']: identification + }; + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(() => __webpack_require__.e(/*! import() | forum/components/SignUpModal */ "forum/components/SignUpModal").then(__webpack_require__.bind(__webpack_require__, /*! ./SignUpModal */ "./src/forum/components/SignUpModal.tsx")), attrs); + } + onready() { + this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').trigger('select'); + } + onsubmit(e) { + e.preventDefault(); + this.loading = true; + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.login(this.loginParams(), { + errorHandler: this.onerror.bind(this) + }).then(() => window.location.reload(), this.loaded.bind(this)); + } + loginParams() { + const data = { + identification: this.identification(), + password: this.password(), + remember: this.remember() + }; + return data; + } + onerror(error) { + if (error.status === 401 && error.alert) { + error.alert.content = _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.log_in.invalid_login_message'); + this.password(''); + } + super.onerror(error); + } +} +flarum.reg.add('core', 'forum/components/LogInModal', LogInModal);flarum.reg.addChunkModule('forum/components/ForgotPasswordModal', './src/forum/components/ForgotPasswordModal.tsx', 'core', 'forum/components/ForgotPasswordModal'); +flarum.reg.addChunkModule('forum/components/SignUpModal', './src/forum/components/SignUpModal.tsx', 'core', 'forum/components/SignUpModal'); + +/***/ }), + +/***/ "./src/forum/components/LogInButtons.js": +/*!**********************************************!*\ + !*** ./src/forum/components/LogInButtons.js ***! + \**********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ LogInButtons) +/* harmony export */ }); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); + + + +/** + * The `LogInButtons` component displays a collection of social login buttons. + */ +class LogInButtons extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + view() { + return m("div", { + className: "LogInButtons" + }, this.items().toArray()); + } + + /** + * Build a list of LogInButton components. + * + * @return {ItemList} + */ + items() { + return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__["default"](); + } +} +flarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons); + +/***/ }) + +}]); //# sourceMappingURL=LogInModal.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/LogInModal.js.map b/framework/core/js/dist/forum/components/LogInModal.js.map index 764569cbcd7..36d23674619 100644 --- a/framework/core/js/dist/forum/components/LogInModal.js.map +++ b/framework/core/js/dist/forum/components/LogInModal.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/LogInModal.js","mappings":"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","sources":["webpack://@flarum/core/./src/forum/components/LogInModal.tsx","webpack://@flarum/core/./src/forum/components/LogInButtons.js"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport LogInButtons from './LogInButtons';\nimport extractText from '../../common/utils/extractText';\nimport ItemList from '../../common/utils/ItemList';\nimport Stream from '../../common/utils/Stream';\nexport default class LogInModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the identification input.\n */\n _defineProperty(this, \"identification\", void 0);\n /**\n * The value of the password input.\n */\n _defineProperty(this, \"password\", void 0);\n /**\n * The value of the remember me input.\n */\n _defineProperty(this, \"remember\", void 0);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.identification = Stream(this.attrs.identification || '');\n this.password = Stream(this.attrs.password || '');\n this.remember = Stream(!!this.attrs.remember);\n }\n className() {\n return 'LogInModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.log_in.title');\n }\n content() {\n return [m(\"div\", {\n className: \"Modal-body\"\n }, this.body()), m(\"div\", {\n className: \"Modal-footer\"\n }, this.footer())];\n }\n body() {\n return [m(LogInButtons, null), m(\"div\", {\n className: \"Form Form--centered\"\n }, this.fields().toArray())];\n }\n fields() {\n const items = new ItemList();\n const identificationLabel = extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'));\n const passwordLabel = extractText(app.translator.trans('core.forum.log_in.password_placeholder'));\n items.add('identification', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"identification\",\n type: \"text\",\n placeholder: identificationLabel,\n \"aria-label\": identificationLabel,\n bidi: this.identification,\n disabled: this.loading\n })), 30);\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"password\",\n type: \"password\",\n autocomplete: \"current-password\",\n placeholder: passwordLabel,\n \"aria-label\": passwordLabel,\n bidi: this.password,\n disabled: this.loading\n })), 20);\n items.add('remember', m(\"div\", {\n className: \"Form-group\"\n }, m(\"div\", null, m(\"label\", {\n className: \"checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n bidi: this.remember,\n disabled: this.loading\n }), app.translator.trans('core.forum.log_in.remember_me_label')))), 10);\n items.add('submit', m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.log_in.submit_button'))), -10);\n return items;\n }\n footer() {\n return m('[', null, m(\"p\", {\n className: \"LogInModal-forgotPassword\"\n }, m(\"a\", {\n onclick: this.forgotPassword.bind(this)\n }, app.translator.trans('core.forum.log_in.forgot_password_link'))), app.forum.attribute('allowSignUp') && m(\"p\", {\n className: \"LogInModal-signUp\"\n }, app.translator.trans('core.forum.log_in.sign_up_text', {\n a: m(\"a\", {\n onclick: this.signUp.bind(this)\n })\n })));\n }\n\n /**\n * Open the forgot password modal, prefilling it with an email if the user has\n * entered one.\n */\n forgotPassword() {\n const email = this.identification();\n const attrs = email.includes('@') ? {\n email\n } : undefined;\n app.modal.show(() => import(/* webpackChunkName: 'forum/components/ForgotPasswordModal', webpackMode: 'lazy-once' */ './ForgotPasswordModal'), attrs);\n }\n\n /**\n * Open the sign up modal, prefilling it with an email/username/password if\n * the user has entered one.\n */\n signUp() {\n const identification = this.identification();\n const attrs = {\n [identification.includes('@') ? 'email' : 'username']: identification\n };\n app.modal.show(() => import(/* webpackChunkName: 'forum/components/SignUpModal', webpackMode: 'lazy-once' */ './SignUpModal'), attrs);\n }\n onready() {\n this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').trigger('select');\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n app.session.login(this.loginParams(), {\n errorHandler: this.onerror.bind(this)\n }).then(() => window.location.reload(), this.loaded.bind(this));\n }\n loginParams() {\n const data = {\n identification: this.identification(),\n password: this.password(),\n remember: this.remember()\n };\n return data;\n }\n onerror(error) {\n if (error.status === 401 && error.alert) {\n error.alert.content = app.translator.trans('core.forum.log_in.invalid_login_message');\n this.password('');\n }\n super.onerror(error);\n }\n}\nflarum.reg.add('core', 'forum/components/LogInModal', LogInModal);flarum.reg.addChunkModule('502', '1839', 'core', 'forum/components/ForgotPasswordModal');\nflarum.reg.addChunkModule('395', '8686', 'core', 'forum/components/SignUpModal');","import Component from '../../common/Component';\nimport ItemList from '../../common/utils/ItemList';\n\n/**\n * The `LogInButtons` component displays a collection of social login buttons.\n */\nexport default class LogInButtons extends Component {\n view() {\n return m(\"div\", {\n className: \"LogInButtons\"\n }, this.items().toArray());\n }\n\n /**\n * Build a list of LogInButton components.\n *\n * @return {ItemList}\n */\n items() {\n return new ItemList();\n }\n}\nflarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons);"],"names":["LogInModal","constructor","super","arguments","this","oninit","vnode","identification","attrs","password","remember","className","title","content","m","body","footer","fields","toArray","items","identificationLabel","passwordLabel","add","name","type","placeholder","bidi","disabled","loading","autocomplete","onclick","forgotPassword","bind","a","signUp","email","includes","undefined","onready","$","trigger","onsubmit","e","preventDefault","loginParams","errorHandler","onerror","then","window","location","reload","loaded","error","status","alert","flarum","reg","addChunkModule","LogInButtons","view"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/LogInModal.js","mappings":";;;;;;;;;;;;;;;;;;;;;AAAwE;AACtC;AACwB;AACN;AACV;AACe;AACN;AACJ;AAChC,yBAAyB,oEAAS;AACjD;AACA;AACA;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,0BAA0B,gEAAM;AAChC,oBAAoB,gEAAM;AAC1B,oBAAoB,gEAAM;AAC1B;AACA;AACA;AACA;AACA;AACA,WAAW,mEAAoB;AAC/B;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA,cAAc,qDAAY;AAC1B;AACA,KAAK;AACL;AACA;AACA,sBAAsB,8DAAQ;AAC9B,gCAAgC,qEAAW,CAAC,mEAAoB;AAChE,0BAA0B,qEAAW,CAAC,mEAAoB;AAC1D;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK,GAAG,mEAAoB;AAC5B;AACA;AACA,KAAK,IAAI,iEAAM;AACf;AACA;AACA;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK,EAAE,mEAAoB,8CAA8C,kEAAmB;AAC5F;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA,OAAO;AACP,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN,IAAI,6DAAc,OAAO,uPAAwH;AACjJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,6DAAc,OAAO,uNAAwG;AACjI;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,gEAAiB;AACrB;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,mEAAoB;AAChD;AACA;AACA;AACA;AACA;AACA,kEAAkE;AAClE;;;;;;;;;;;;;;;;AC7J+C;AACI;;AAEnD;AACA;AACA;AACe,2BAA2B,yDAAS;AACnD;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA,eAAe,8DAAQ;AACvB;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/LogInModal.tsx","webpack://@flarum/core/./src/forum/components/LogInButtons.js"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport LogInButtons from './LogInButtons';\nimport extractText from '../../common/utils/extractText';\nimport ItemList from '../../common/utils/ItemList';\nimport Stream from '../../common/utils/Stream';\nexport default class LogInModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the identification input.\n */\n _defineProperty(this, \"identification\", void 0);\n /**\n * The value of the password input.\n */\n _defineProperty(this, \"password\", void 0);\n /**\n * The value of the remember me input.\n */\n _defineProperty(this, \"remember\", void 0);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.identification = Stream(this.attrs.identification || '');\n this.password = Stream(this.attrs.password || '');\n this.remember = Stream(!!this.attrs.remember);\n }\n className() {\n return 'LogInModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.log_in.title');\n }\n content() {\n return [m(\"div\", {\n className: \"Modal-body\"\n }, this.body()), m(\"div\", {\n className: \"Modal-footer\"\n }, this.footer())];\n }\n body() {\n return [m(LogInButtons, null), m(\"div\", {\n className: \"Form Form--centered\"\n }, this.fields().toArray())];\n }\n fields() {\n const items = new ItemList();\n const identificationLabel = extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'));\n const passwordLabel = extractText(app.translator.trans('core.forum.log_in.password_placeholder'));\n items.add('identification', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"identification\",\n type: \"text\",\n placeholder: identificationLabel,\n \"aria-label\": identificationLabel,\n bidi: this.identification,\n disabled: this.loading\n })), 30);\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"password\",\n type: \"password\",\n autocomplete: \"current-password\",\n placeholder: passwordLabel,\n \"aria-label\": passwordLabel,\n bidi: this.password,\n disabled: this.loading\n })), 20);\n items.add('remember', m(\"div\", {\n className: \"Form-group\"\n }, m(\"div\", null, m(\"label\", {\n className: \"checkbox\"\n }, m(\"input\", {\n type: \"checkbox\",\n bidi: this.remember,\n disabled: this.loading\n }), app.translator.trans('core.forum.log_in.remember_me_label')))), 10);\n items.add('submit', m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.log_in.submit_button'))), -10);\n return items;\n }\n footer() {\n return m('[', null, m(\"p\", {\n className: \"LogInModal-forgotPassword\"\n }, m(\"a\", {\n onclick: this.forgotPassword.bind(this)\n }, app.translator.trans('core.forum.log_in.forgot_password_link'))), app.forum.attribute('allowSignUp') && m(\"p\", {\n className: \"LogInModal-signUp\"\n }, app.translator.trans('core.forum.log_in.sign_up_text', {\n a: m(\"a\", {\n onclick: this.signUp.bind(this)\n })\n })));\n }\n\n /**\n * Open the forgot password modal, prefilling it with an email if the user has\n * entered one.\n */\n forgotPassword() {\n const email = this.identification();\n const attrs = email.includes('@') ? {\n email\n } : undefined;\n app.modal.show(() => import(/* webpackChunkName: 'forum/components/ForgotPasswordModal', webpackMode: 'lazy-once' */ './ForgotPasswordModal'), attrs);\n }\n\n /**\n * Open the sign up modal, prefilling it with an email/username/password if\n * the user has entered one.\n */\n signUp() {\n const identification = this.identification();\n const attrs = {\n [identification.includes('@') ? 'email' : 'username']: identification\n };\n app.modal.show(() => import(/* webpackChunkName: 'forum/components/SignUpModal', webpackMode: 'lazy-once' */ './SignUpModal'), attrs);\n }\n onready() {\n this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').trigger('select');\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n app.session.login(this.loginParams(), {\n errorHandler: this.onerror.bind(this)\n }).then(() => window.location.reload(), this.loaded.bind(this));\n }\n loginParams() {\n const data = {\n identification: this.identification(),\n password: this.password(),\n remember: this.remember()\n };\n return data;\n }\n onerror(error) {\n if (error.status === 401 && error.alert) {\n error.alert.content = app.translator.trans('core.forum.log_in.invalid_login_message');\n this.password('');\n }\n super.onerror(error);\n }\n}\nflarum.reg.add('core', 'forum/components/LogInModal', LogInModal);flarum.reg.addChunkModule('forum/components/ForgotPasswordModal', './src/forum/components/ForgotPasswordModal.tsx', 'core', 'forum/components/ForgotPasswordModal');\nflarum.reg.addChunkModule('forum/components/SignUpModal', './src/forum/components/SignUpModal.tsx', 'core', 'forum/components/SignUpModal');","import Component from '../../common/Component';\nimport ItemList from '../../common/utils/ItemList';\n\n/**\n * The `LogInButtons` component displays a collection of social login buttons.\n */\nexport default class LogInButtons extends Component {\n view() {\n return m(\"div\", {\n className: \"LogInButtons\"\n }, this.items().toArray());\n }\n\n /**\n * Build a list of LogInButton components.\n *\n * @return {ItemList}\n */\n items() {\n return new ItemList();\n }\n}\nflarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/NotificationsPage.js b/framework/core/js/dist/forum/components/NotificationsPage.js index 13adcf01c83..8f96056397e 100644 --- a/framework/core/js/dist/forum/components/NotificationsPage.js +++ b/framework/core/js/dist/forum/components/NotificationsPage.js @@ -1,2 +1,47 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[744],{8246:(i,t,o)=>{o.r(t),o.d(t,{default:()=>r});var s=o(6789),a=o(4661),n=o(7297),e=o(1552);class r extends a.Z{oninit(i){super.oninit(i),s.Z.history.push("notifications",(0,e.Z)(s.Z.translator.trans("core.forum.notifications.title"))),s.Z.notifications.load(),this.bodyClass="App--notifications"}view(){return m("div",{className:"NotificationsPage"},m(n.Z,{state:s.Z.notifications}))}}flarum.reg.add("core","forum/components/NotificationsPage",r)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/NotificationsPage"],{ + +/***/ "./src/forum/components/NotificationsPage.tsx": +/*!****************************************************!*\ + !*** ./src/forum/components/NotificationsPage.tsx ***! + \****************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ NotificationsPage) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_Page__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/Page */ "./src/common/components/Page.tsx"); +/* harmony import */ var _NotificationList__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./NotificationList */ "./src/forum/components/NotificationList.js"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); + + + + + +/** + * The `NotificationsPage` component shows the notifications list. It is only + * used on mobile devices where the notifications dropdown is within the drawer. + */ +class NotificationsPage extends _common_components_Page__WEBPACK_IMPORTED_MODULE_1__["default"] { + oninit(vnode) { + super.oninit(vnode); + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].history.push('notifications', (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_3__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.notifications.title'))); + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].notifications.load(); + this.bodyClass = 'App--notifications'; + } + view() { + return m("div", { + className: "NotificationsPage" + }, m(_NotificationList__WEBPACK_IMPORTED_MODULE_2__["default"], { + state: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].notifications + })); + } +} +flarum.reg.add('core', 'forum/components/NotificationsPage', NotificationsPage); + +/***/ }) + +}]); //# sourceMappingURL=NotificationsPage.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/NotificationsPage.js.map b/framework/core/js/dist/forum/components/NotificationsPage.js.map index d20d7046b4f..9f0fcf88e37 100644 --- a/framework/core/js/dist/forum/components/NotificationsPage.js.map +++ b/framework/core/js/dist/forum/components/NotificationsPage.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/NotificationsPage.js","mappings":"mLASe,MAAMA,UAA0B,IAC7CC,OAAOC,GACLC,MAAMF,OAAOC,GACb,iBAAiB,iBAAiB,OAAY,qBAAqB,oCACnE,yBACAE,KAAKC,UAAY,oBACnB,CACAC,OACE,OAAOC,EAAE,MAAO,CACdC,UAAW,qBACVD,EAAE,IAAkB,CACrBE,MAAO,oBAEX,EAEFC,OAAOC,IAAIC,IAAI,OAAQ,qCAAsCZ,E","sources":["webpack://@flarum/core/./src/forum/components/NotificationsPage.tsx"],"sourcesContent":["import app from '../../forum/app';\nimport Page from '../../common/components/Page';\nimport NotificationList from './NotificationList';\nimport extractText from '../../common/utils/extractText';\n\n/**\n * The `NotificationsPage` component shows the notifications list. It is only\n * used on mobile devices where the notifications dropdown is within the drawer.\n */\nexport default class NotificationsPage extends Page {\n oninit(vnode) {\n super.oninit(vnode);\n app.history.push('notifications', extractText(app.translator.trans('core.forum.notifications.title')));\n app.notifications.load();\n this.bodyClass = 'App--notifications';\n }\n view() {\n return m(\"div\", {\n className: \"NotificationsPage\"\n }, m(NotificationList, {\n state: app.notifications\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/NotificationsPage', NotificationsPage);"],"names":["NotificationsPage","oninit","vnode","super","this","bodyClass","view","m","className","state","flarum","reg","add"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/NotificationsPage.js","mappings":";;;;;;;;;;;;;;;;;AAAkC;AACc;AACE;AACO;;AAEzD;AACA;AACA;AACA;AACe,gCAAgC,+DAAI;AACnD;AACA;AACA,IAAI,+DAAgB,kBAAkB,qEAAW,CAAC,mEAAoB;AACtE,IAAI,qEAAsB;AAC1B;AACA;AACA;AACA;AACA;AACA,KAAK,IAAI,yDAAgB;AACzB,aAAa,gEAAiB;AAC9B,KAAK;AACL;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/NotificationsPage.tsx"],"sourcesContent":["import app from '../../forum/app';\nimport Page from '../../common/components/Page';\nimport NotificationList from './NotificationList';\nimport extractText from '../../common/utils/extractText';\n\n/**\n * The `NotificationsPage` component shows the notifications list. It is only\n * used on mobile devices where the notifications dropdown is within the drawer.\n */\nexport default class NotificationsPage extends Page {\n oninit(vnode) {\n super.oninit(vnode);\n app.history.push('notifications', extractText(app.translator.trans('core.forum.notifications.title')));\n app.notifications.load();\n this.bodyClass = 'App--notifications';\n }\n view() {\n return m(\"div\", {\n className: \"NotificationsPage\"\n }, m(NotificationList, {\n state: app.notifications\n }));\n }\n}\nflarum.reg.add('core', 'forum/components/NotificationsPage', NotificationsPage);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/PostStream.js b/framework/core/js/dist/forum/components/PostStream.js index 374a7ee0cb4..11c50acb6e8 100644 --- a/framework/core/js/dist/forum/components/PostStream.js +++ b/framework/core/js/dist/forum/components/PostStream.js @@ -1,2 +1,590 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[563],{7764:(t,s,e)=>{e.r(s),e.d(s,{default:()=>g});var a=e(6789),o=e(2190),i=e(4718),r=e(7323);class n extends o.Z{view(){return m("div",{className:"Post CommentPost LoadingPost"},m("header",{className:"Post-header"},m(r.Z,{user:null,className:"PostUser-avatar"}),m("div",{className:"fakeText"})),m("div",{className:"Post-body"},m("div",{className:"fakeText"}),m("div",{className:"fakeText"}),m("div",{className:"fakeText"})))}}flarum.reg.add("core","forum/components/LoadingPost",n);var l=e(5537),d=e(3666),c=e(378),h=e(1268);class u extends o.Z{view(){return a.Z.composer.composingReplyTo(this.attrs.discussion)?m("article",{className:"Post CommentPost editing","aria-busy":"true"},m("div",{className:"Post-container"},m("div",{className:"Post-side"},m(r.Z,{user:a.Z.session.user,className:"Post-avatar"})),m("div",{className:"Post-main"},m("header",{className:"Post-header"},m("div",{className:"PostUser"},m("h3",{className:"PostUser-name"},(0,l.Z)(a.Z.session.user)),m("ul",{className:"PostUser-badges badges badges--packed"},(0,h.Z)(a.Z.session.user.badges().toArray())))),m("div",{className:"Post-body"},m(c.Z,{className:"Post-body",composer:a.Z.composer,surround:this.anchorPreview.bind(this)}))))):m("button",{className:"Post ReplyPlaceholder",onclick:()=>{d.Z.replyAction.call(this.attrs.discussion,!0).catch((()=>{}))}},m("div",{className:"Post-container"},m("div",{className:"Post-side"},m(r.Z,{user:a.Z.session.user,className:"Post-avatar"})),m("div",{className:"Post-main"},m("span",{className:"Post-header"},a.Z.translator.trans("core.forum.post_stream.reply_placeholder")))))}anchorPreview(t){const s=$(window).scrollTop()+$(window).height()>=$(document).height();t(),s&&$(window).scrollTop($(document).height())}}flarum.reg.add("core","forum/components/ReplyPlaceholder",u);var p=e(8312),f=e(4041);class g extends o.Z{oninit(t){super.oninit(t),this.discussion=this.attrs.discussion,this.stream=this.attrs.stream,this.scrollListener=new i.Z(this.onscroll.bind(this))}view(){let t;const s=this.stream.viewingEnd(),e=this.stream.posts(),o=this.discussion.postIds(),i=t=>{$(t.dom).addClass("fadeIn"),setTimeout((()=>$(t.dom).removeClass("fadeIn")),500)},r=e.map(((s,e)=>{let r;const l={"data-index":this.stream.visibleStart+e};if(s){const e=s.createdAt(),o=a.Z.postComponents[s.contentType()];r=!!o&&m(o,{post:s}),l.key="post"+s.id(),l.oncreate=i,l["data-time"]=e.toISOString(),l["data-number"]=s.number(),l["data-id"]=s.id(),l["data-type"]=s.contentType();const n=e-t;n>3456e5&&(r=[m("div",{className:"PostStream-timeGap"},m("span",null,a.Z.translator.trans("core.forum.post_stream.time_lapsed_text",{period:dayjs().add(n,"ms").fromNow(!0)}))),r]),t=e}else l.key="post"+o[this.stream.visibleStart+e],r=m(n,null);return m("div",Object.assign({className:"PostStream-item"},l),r)}));return!s&&e[this.stream.visibleEnd-this.stream.visibleStart-1]&&r.push(m("div",{className:"PostStream-loadMore",key:"loadMore"},m(p.Z,{className:"Button",onclick:this.stream.loadNext.bind(this.stream)},a.Z.translator.trans("core.forum.post_stream.load_more_button")))),s&&r.push(...this.endItems().toArray()),!s||a.Z.session.user&&!this.discussion.canReply()||r.push(m("div",{className:"PostStream-item",key:"reply","data-index":this.stream.count(),oncreate:i},m(u,{discussion:this.discussion}))),m("div",{className:"PostStream",role:"feed","aria-live":"off","aria-busy":this.stream.pagesLoading?"true":"false"},r)}endItems(){return new f.Z}onupdate(t){super.onupdate(t),this.triggerScroll()}oncreate(t){super.oncreate(t),this.triggerScroll(),setTimeout((()=>this.scrollListener.start()))}onremove(t){super.onremove(t),this.scrollListener.stop(),clearTimeout(this.calculatePositionTimeout)}triggerScroll(){if(!this.stream.needsScroll)return;const t=this.stream.targetPost;this.stream.needsScroll=!1,"number"in t?this.scrollToNumber(t.number,this.stream.animateScroll):"index"in t&&this.scrollToIndex(t.index,this.stream.animateScroll,t.reply)}onscroll(t){void 0===t&&(t=window.pageYOffset),this.stream.paused||this.stream.pagesLoading||(this.updateScrubber(t),this.loadPostsIfNeeded(t),clearTimeout(this.calculatePositionTimeout),this.calculatePositionTimeout=setTimeout(this.calculatePosition.bind(this,t),100))}loadPostsIfNeeded(t){void 0===t&&(t=window.pageYOffset);const s=this.getMarginTop(),e=$(window).height()-s,a=t+s;if(this.stream.visibleStart>0){const t=this.$(".PostStream-item[data-index="+this.stream.visibleStart+"]");t.length&&t.offset().top>a-300&&this.stream.loadPrevious()}if(this.stream.visibleEnda+e)return!1;const m=Math.max(0,a-s),l=Math.min(o,a+e-s)-m;null===n&&(n=parseFloat(t.data("index"))+m/o),l>0&&(i+=l/o);const d=t.data("time");d&&(r=d)})),this.stream.index=null!==n?n+1:this.stream.count(),this.stream.visible=i,r&&(this.stream.description=dayjs(r).format("MMMM YYYY"))}calculatePosition(t){void 0===t&&(t=window.pageYOffset);const s=this.getMarginTop(),e=$(window),a=e.height()-s,o=e.scrollTop()+s,i=t+s;let r,n;this.$(".PostStream-item").each((function(){const t=$(this),s=t.offset().top,e=t.outerHeight(!0),m=Math.max(0,i-s);if(void 0===r&&(m/e<.75||(e-m)/a>.25)&&(r=t.data("number")),s+e>o){if(!(s+el){const e=o?n-$(window).height()+a.Z.composer.computedHeight():t.is(":first-child")?0:r;s?e!==m&&i.animate({scrollTop:e},"fast"):i.scrollTop(e)}}const n=()=>{this.updateScrubber(),void 0!==r&&(this.stream.index=r+1)};return n(),this.stream.forceUpdateScrubber=!0,Promise.all([i.promise(),this.stream.loadPromise]).then((()=>{let t;if(m.redraw.sync(),o){const t=$(".PostStream-item:last-child");$(window).scrollTop(t.offset().top+t.height()-$(window).height()+a.Z.composer.computedHeight())}else 0===r?$(window).scrollTop(0):(t=$(".PostStream-item[data-index=".concat(r,"]")).offset())&&$(window).scrollTop(t.top-this.getMarginTop());n(),this.calculatePosition(),this.stream.paused=!1,this.loadPostsIfNeeded()}))}flashItem(t){t.removeClass("fadeIn"),t.addClass("flash").on("animationend webkitAnimationEnd",(s=>{t.removeClass("flash")}))}}flarum.reg.add("core","forum/components/PostStream",g)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/PostStream"],{ + +/***/ "./src/forum/components/LoadingPost.js": +/*!*********************************************!*\ + !*** ./src/forum/components/LoadingPost.js ***! + \*********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ LoadingPost) +/* harmony export */ }); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx"); + + + +/** + * The `LoadingPost` component shows a placeholder that looks like a post, + * indicating that the post is loading. + */ +class LoadingPost extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + view() { + return m("div", { + className: "Post CommentPost LoadingPost" + }, m("header", { + className: "Post-header" + }, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_1__["default"], { + user: null, + className: "PostUser-avatar" + }), m("div", { + className: "fakeText" + })), m("div", { + className: "Post-body" + }, m("div", { + className: "fakeText" + }), m("div", { + className: "fakeText" + }), m("div", { + className: "fakeText" + }))); + } +} +flarum.reg.add('core', 'forum/components/LoadingPost', LoadingPost); + +/***/ }), + +/***/ "./src/forum/components/PostStream.js": +/*!********************************************!*\ + !*** ./src/forum/components/PostStream.js ***! + \********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ PostStream) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_utils_ScrollListener__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/utils/ScrollListener */ "./src/common/utils/ScrollListener.js"); +/* harmony import */ var _LoadingPost__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./LoadingPost */ "./src/forum/components/LoadingPost.js"); +/* harmony import */ var _ReplyPlaceholder__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./ReplyPlaceholder */ "./src/forum/components/ReplyPlaceholder.js"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); + + + + + + + + +/** + * The `PostStream` component displays an infinitely-scrollable wall of posts in + * a discussion. Posts that have not loaded will be displayed as placeholders. + * + * ### Attrs + * + * - `discussion` + * - `stream` + * - `targetPost` + * - `onPositionChange` + */ +class PostStream extends _common_Component__WEBPACK_IMPORTED_MODULE_1__["default"] { + oninit(vnode) { + super.oninit(vnode); + this.discussion = this.attrs.discussion; + this.stream = this.attrs.stream; + this.scrollListener = new _common_utils_ScrollListener__WEBPACK_IMPORTED_MODULE_2__["default"](this.onscroll.bind(this)); + } + view() { + let lastTime; + const viewingEnd = this.stream.viewingEnd(); + const posts = this.stream.posts(); + const postIds = this.discussion.postIds(); + const postFadeIn = vnode => { + $(vnode.dom).addClass('fadeIn'); + // 500 is the duration of the fadeIn CSS animation + 100ms, + // so the animation has time to complete + setTimeout(() => $(vnode.dom).removeClass('fadeIn'), 500); + }; + const items = posts.map((post, i) => { + let content; + const attrs = { + 'data-index': this.stream.visibleStart + i + }; + if (post) { + const time = post.createdAt(); + const PostComponent = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].postComponents[post.contentType()]; + content = !!PostComponent && m(PostComponent, { + post: post + }); + attrs.key = 'post' + post.id(); + attrs.oncreate = postFadeIn; + attrs['data-time'] = time.toISOString(); + attrs['data-number'] = post.number(); + attrs['data-id'] = post.id(); + attrs['data-type'] = post.contentType(); + + // If the post before this one was more than 4 days ago, we will + // display a 'time gap' indicating how long it has been in between + // the posts. + const dt = time - lastTime; + if (dt > 1000 * 60 * 60 * 24 * 4) { + content = [m("div", { + className: "PostStream-timeGap" + }, m("span", null, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.post_stream.time_lapsed_text', { + period: dayjs().add(dt, 'ms').fromNow(true) + }))), content]; + } + lastTime = time; + } else { + attrs.key = 'post' + postIds[this.stream.visibleStart + i]; + content = m(_LoadingPost__WEBPACK_IMPORTED_MODULE_3__["default"], null); + } + return m("div", Object.assign({ + className: "PostStream-item" + }, attrs), content); + }); + if (!viewingEnd && posts[this.stream.visibleEnd - this.stream.visibleStart - 1]) { + items.push(m("div", { + className: "PostStream-loadMore", + key: "loadMore" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_5__["default"], { + className: "Button", + onclick: this.stream.loadNext.bind(this.stream) + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.post_stream.load_more_button')))); + } + + // Allow extensions to add items to the end of the post stream. + if (viewingEnd) { + items.push(...this.endItems().toArray()); + } + + // If we're viewing the end of the discussion, the user can reply, and + // is not already doing so, then show a 'write a reply' placeholder. + if (viewingEnd && (!_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].session.user || this.discussion.canReply())) { + items.push(m("div", { + className: "PostStream-item", + key: "reply", + "data-index": this.stream.count(), + oncreate: postFadeIn + }, m(_ReplyPlaceholder__WEBPACK_IMPORTED_MODULE_4__["default"], { + discussion: this.discussion + }))); + } + return m("div", { + className: "PostStream", + role: "feed", + "aria-live": "off", + "aria-busy": this.stream.pagesLoading ? 'true' : 'false' + }, items); + } + + /** + * @returns {ItemList} + */ + endItems() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__["default"](); + return items; + } + onupdate(vnode) { + super.onupdate(vnode); + this.triggerScroll(); + } + oncreate(vnode) { + super.oncreate(vnode); + this.triggerScroll(); + + // This is wrapped in setTimeout due to the following Mithril issue: + // https://github.com/lhorie/mithril.js/issues/637 + setTimeout(() => this.scrollListener.start()); + } + onremove(vnode) { + super.onremove(vnode); + this.scrollListener.stop(); + clearTimeout(this.calculatePositionTimeout); + } + + /** + * Start scrolling, if appropriate, to a newly-targeted post. + */ + triggerScroll() { + if (!this.stream.needsScroll) return; + const target = this.stream.targetPost; + this.stream.needsScroll = false; + if ('number' in target) { + this.scrollToNumber(target.number, this.stream.animateScroll); + } else if ('index' in target) { + this.scrollToIndex(target.index, this.stream.animateScroll, target.reply); + } + } + + /** + * + * @param {number} top + */ + onscroll(top) { + if (top === void 0) { + top = window.pageYOffset; + } + if (this.stream.paused || this.stream.pagesLoading) return; + this.updateScrubber(top); + this.loadPostsIfNeeded(top); + + // Throttle calculation of our position (start/end numbers of posts in the + // viewport) to 100ms. + clearTimeout(this.calculatePositionTimeout); + this.calculatePositionTimeout = setTimeout(this.calculatePosition.bind(this, top), 100); + } + + /** + * Check if either extreme of the post stream is in the viewport, + * and if so, trigger loading the next/previous page. + * + * @param {number} top + */ + loadPostsIfNeeded(top) { + if (top === void 0) { + top = window.pageYOffset; + } + const marginTop = this.getMarginTop(); + const viewportHeight = $(window).height() - marginTop; + const viewportTop = top + marginTop; + const loadAheadDistance = 300; + if (this.stream.visibleStart > 0) { + const $item = this.$('.PostStream-item[data-index=' + this.stream.visibleStart + ']'); + if ($item.length && $item.offset().top > viewportTop - loadAheadDistance) { + this.stream.loadPrevious(); + } + } + if (this.stream.visibleEnd < this.stream.count()) { + const $item = this.$('.PostStream-item[data-index=' + (this.stream.visibleEnd - 1) + ']'); + if ($item.length && $item.offset().top + $item.outerHeight(true) < viewportTop + viewportHeight + loadAheadDistance) { + this.stream.loadNext(); + } + } + } + updateScrubber(top) { + if (top === void 0) { + top = window.pageYOffset; + } + const marginTop = this.getMarginTop(); + const viewportHeight = $(window).height() - marginTop; + const viewportTop = top + marginTop; + + // Before looping through all of the posts, we reset the scrollbar + // properties to a 'default' state. These values reflect what would be + // seen if the browser were scrolled right up to the top of the page, + // and the viewport had a height of 0. + const $items = this.$('.PostStream-item[data-index]'); + let visible = 0; + let period = ''; + let indexFromViewPort = null; + + // Now loop through each of the items in the discussion. An 'item' is + // either a single post or a 'gap' of one or more posts that haven't + // been loaded yet. + $items.each(function () { + const $this = $(this); + const top = $this.offset().top; + const height = $this.outerHeight(true); + + // If this item is above the top of the viewport, skip to the next + // one. If it's below the bottom of the viewport, break out of the + // loop. + if (top + height < viewportTop) { + return true; + } + if (top > viewportTop + viewportHeight) { + return false; + } + + // Work out how many pixels of this item are visible inside the viewport. + // Then add the proportion of this item's total height to the index. + const visibleTop = Math.max(0, viewportTop - top); + const visibleBottom = Math.min(height, viewportTop + viewportHeight - top); + const visiblePost = visibleBottom - visibleTop; + + // We take the index of the first item that passed the previous checks. + // It is the item that is first visible in the viewport. + if (indexFromViewPort === null) { + indexFromViewPort = parseFloat($this.data('index')) + visibleTop / height; + } + if (visiblePost > 0) { + visible += visiblePost / height; + } + + // If this item has a time associated with it, then set the + // scrollbar's current period to a formatted version of this time. + const time = $this.data('time'); + if (time) period = time; + }); + + // If indexFromViewPort is null, it means no posts are visible in the + // viewport. This can happen, when drafting a long reply post. In that case + // set the index to the last post. + this.stream.index = indexFromViewPort !== null ? indexFromViewPort + 1 : this.stream.count(); + this.stream.visible = visible; + if (period) this.stream.description = dayjs(period).format('MMMM YYYY'); + } + + /** + * Work out which posts (by number) are currently visible in the viewport, and + * fire an event with the information. + */ + calculatePosition(top) { + if (top === void 0) { + top = window.pageYOffset; + } + const marginTop = this.getMarginTop(); + const $window = $(window); + const viewportHeight = $window.height() - marginTop; + const scrollTop = $window.scrollTop() + marginTop; + const viewportTop = top + marginTop; + let startNumber; + let endNumber; + this.$('.PostStream-item').each(function () { + const $item = $(this); + const top = $item.offset().top; + const height = $item.outerHeight(true); + const visibleTop = Math.max(0, viewportTop - top); + const threeQuartersVisible = visibleTop / height < 0.75; + const coversQuarterOfViewport = (height - visibleTop) / viewportHeight > 0.25; + if (startNumber === undefined && (threeQuartersVisible || coversQuarterOfViewport)) { + startNumber = $item.data('number'); + } + if (top + height > scrollTop) { + if (top + height < scrollTop + viewportHeight) { + if ($item.data('number')) { + endNumber = $item.data('number'); + } + } else return false; + } + }); + if (startNumber) { + this.attrs.onPositionChange(startNumber || 1, endNumber, startNumber); + } + } + + /** + * Get the distance from the top of the viewport to the point at which we + * would consider a post to be the first one visible. + * + * @return {number} + */ + getMarginTop() { + const headerId = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].screen() === 'phone' ? '#app-navigation' : '#header'; + return this.$() && $(headerId).outerHeight() + parseInt(this.$().css('margin-top'), 10); + } + + /** + * Scroll down to a certain post by number and 'flash' it. + * + * @param {number} number + * @param {boolean} animate + * @return {JQueryDeferred} + */ + scrollToNumber(number, animate) { + const $item = this.$(".PostStream-item[data-number=".concat(number, "]")); + return this.scrollToItem($item, animate).then(this.flashItem.bind(this, $item)); + } + + /** + * Scroll down to a certain post by index. + * + * @param {number} index + * @param {boolean} animate + * @param {boolean} reply Whether or not to scroll to the reply placeholder. + * @return {JQueryDeferred} + */ + scrollToIndex(index, animate, reply) { + const $item = reply ? $('.PostStream-item:last-child') : this.$(".PostStream-item[data-index=".concat(index, "]")); + this.scrollToItem($item, animate, true, reply); + if (reply) { + this.flashItem($item); + } + } + + /** + * Scroll down to the given post. + * + * @param {JQuery} $item + * @param {boolean} animate + * @param {boolean} force Whether or not to force scrolling to the item, even + * if it is already in the viewport. + * @param {boolean} reply Whether or not to scroll to the reply placeholder. + * @return {JQueryDeferred} + */ + scrollToItem($item, animate, force, reply) { + const $container = $('html, body').stop(true); + const index = $item.data('index'); + if ($item.length) { + const itemTop = $item.offset().top - this.getMarginTop(); + const itemBottom = $item.offset().top + $item.height(); + const scrollTop = $(document).scrollTop(); + const scrollBottom = scrollTop + $(window).height(); + + // If the item is already in the viewport, we may not need to scroll. + // If we're scrolling to the reply placeholder, we'll make sure its + // bottom will line up with the top of the composer. + if (force || itemTop < scrollTop || itemBottom > scrollBottom) { + const top = reply ? itemBottom - $(window).height() + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop; + if (!animate) { + $container.scrollTop(top); + } else if (top !== scrollTop) { + $container.animate({ + scrollTop: top + }, 'fast'); + } + } + } + const updateScrubberHeight = () => { + // We manually set the index because we want to display the index of the + // exact post we've scrolled to, not just that of the first post within viewport. + this.updateScrubber(); + if (index !== undefined) this.stream.index = index + 1; + }; + + // If we don't update this before the scroll, the scrubber will start + // at the top, and animate down, which can be confusing + updateScrubberHeight(); + this.stream.forceUpdateScrubber = true; + return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => { + m.redraw.sync(); + + // Rendering post contents will probably throw off our position. + // To counter this, we'll scroll either: + // - To the reply placeholder (aligned with composer top) + // - To the top of the page if we're on the first post + // - To the top of a post (if that post exists) + // If the post does not currently exist, it's probably + // outside of the range we loaded in, so we won't adjust anything, + // as it will soon be rendered by the "load more" system. + let itemOffset; + if (reply) { + const $placeholder = $('.PostStream-item:last-child'); + $(window).scrollTop($placeholder.offset().top + $placeholder.height() - $(window).height() + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.computedHeight()); + } else if (index === 0) { + $(window).scrollTop(0); + } else if (itemOffset = $(".PostStream-item[data-index=".concat(index, "]")).offset()) { + $(window).scrollTop(itemOffset.top - this.getMarginTop()); + } + + // We want to adjust this again after posts have been loaded in + // and position adjusted so that the scrubber's height is accurate. + updateScrubberHeight(); + this.calculatePosition(); + this.stream.paused = false; + // Check if we need to load more posts after scrolling. + this.loadPostsIfNeeded(); + }); + } + + /** + * 'Flash' the given post, drawing the user's attention to it. + * + * @param {JQuery} $item + */ + flashItem($item) { + // This might execute before the fadeIn class has been removed in PostStreamItem's + // oncreate, so we remove it just to be safe and avoid a double animation. + $item.removeClass('fadeIn'); + $item.addClass('flash').on('animationend webkitAnimationEnd', e => { + $item.removeClass('flash'); + }); + } +} +flarum.reg.add('core', 'forum/components/PostStream', PostStream); + +/***/ }), + +/***/ "./src/forum/components/ReplyPlaceholder.js": +/*!**************************************************!*\ + !*** ./src/forum/components/ReplyPlaceholder.js ***! + \**************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ReplyPlaceholder) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_helpers_username__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/helpers/username */ "./src/common/helpers/username.tsx"); +/* harmony import */ var _utils_DiscussionControls__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../utils/DiscussionControls */ "./src/forum/utils/DiscussionControls.js"); +/* harmony import */ var _ComposerPostPreview__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./ComposerPostPreview */ "./src/forum/components/ComposerPostPreview.js"); +/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx"); +/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx"); + + + + + + + + +/** + * The `ReplyPlaceholder` component displays a placeholder for a reply, which, + * when clicked, opens the reply composer. + * + * ### Attrs + * + * - `discussion` + */ +class ReplyPlaceholder extends _common_Component__WEBPACK_IMPORTED_MODULE_1__["default"] { + view() { + if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.composingReplyTo(this.attrs.discussion)) { + return m("article", { + className: "Post CommentPost editing", + "aria-busy": "true" + }, m("div", { + className: "Post-container" + }, m("div", { + className: "Post-side" + }, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_6__["default"], { + user: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].session.user, + className: "Post-avatar" + })), m("div", { + className: "Post-main" + }, m("header", { + className: "Post-header" + }, m("div", { + className: "PostUser" + }, m("h3", { + className: "PostUser-name" + }, (0,_common_helpers_username__WEBPACK_IMPORTED_MODULE_2__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].session.user)), m("ul", { + className: "PostUser-badges badges badges--packed" + }, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].session.user.badges().toArray())))), m("div", { + className: "Post-body" + }, m(_ComposerPostPreview__WEBPACK_IMPORTED_MODULE_4__["default"], { + className: "Post-body", + composer: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer, + surround: this.anchorPreview.bind(this) + }))))); + } + const reply = () => { + _utils_DiscussionControls__WEBPACK_IMPORTED_MODULE_3__["default"].replyAction.call(this.attrs.discussion, true).catch(() => {}); + }; + return m("button", { + className: "Post ReplyPlaceholder", + onclick: reply + }, m("div", { + className: "Post-container" + }, m("div", { + className: "Post-side" + }, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_6__["default"], { + user: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].session.user, + className: "Post-avatar" + })), m("div", { + className: "Post-main" + }, m("span", { + className: "Post-header" + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.post_stream.reply_placeholder'))))); + } + anchorPreview(preview) { + const anchorToBottom = $(window).scrollTop() + $(window).height() >= $(document).height(); + preview(); + if (anchorToBottom) { + $(window).scrollTop($(document).height()); + } + } +} +flarum.reg.add('core', 'forum/components/ReplyPlaceholder', ReplyPlaceholder); + +/***/ }) + +}]); //# sourceMappingURL=PostStream.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/PostStream.js.map b/framework/core/js/dist/forum/components/PostStream.js.map index 3584bee15e4..fa16e842b10 100644 --- a/framework/core/js/dist/forum/components/PostStream.js.map +++ b/framework/core/js/dist/forum/components/PostStream.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/PostStream.js","mappings":"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","sources":["webpack://@flarum/core/./src/forum/components/LoadingPost.js","webpack://@flarum/core/./src/forum/components/ReplyPlaceholder.js","webpack://@flarum/core/./src/forum/components/PostStream.js"],"sourcesContent":["import Component from '../../common/Component';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `LoadingPost` component shows a placeholder that looks like a post,\n * indicating that the post is loading.\n */\nexport default class LoadingPost extends Component {\n view() {\n return m(\"div\", {\n className: \"Post CommentPost LoadingPost\"\n }, m(\"header\", {\n className: \"Post-header\"\n }, m(Avatar, {\n user: null,\n className: \"PostUser-avatar\"\n }), m(\"div\", {\n className: \"fakeText\"\n })), m(\"div\", {\n className: \"Post-body\"\n }, m(\"div\", {\n className: \"fakeText\"\n }), m(\"div\", {\n className: \"fakeText\"\n }), m(\"div\", {\n className: \"fakeText\"\n })));\n }\n}\nflarum.reg.add('core', 'forum/components/LoadingPost', LoadingPost);","import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport username from '../../common/helpers/username';\nimport DiscussionControls from '../utils/DiscussionControls';\nimport ComposerPostPreview from './ComposerPostPreview';\nimport listItems from '../../common/helpers/listItems';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ReplyPlaceholder` component displays a placeholder for a reply, which,\n * when clicked, opens the reply composer.\n *\n * ### Attrs\n *\n * - `discussion`\n */\nexport default class ReplyPlaceholder extends Component {\n view() {\n if (app.composer.composingReplyTo(this.attrs.discussion)) {\n return m(\"article\", {\n className: \"Post CommentPost editing\",\n \"aria-busy\": \"true\"\n }, m(\"div\", {\n className: \"Post-container\"\n }, m(\"div\", {\n className: \"Post-side\"\n }, m(Avatar, {\n user: app.session.user,\n className: \"Post-avatar\"\n })), m(\"div\", {\n className: \"Post-main\"\n }, m(\"header\", {\n className: \"Post-header\"\n }, m(\"div\", {\n className: \"PostUser\"\n }, m(\"h3\", {\n className: \"PostUser-name\"\n }, username(app.session.user)), m(\"ul\", {\n className: \"PostUser-badges badges badges--packed\"\n }, listItems(app.session.user.badges().toArray())))), m(\"div\", {\n className: \"Post-body\"\n }, m(ComposerPostPreview, {\n className: \"Post-body\",\n composer: app.composer,\n surround: this.anchorPreview.bind(this)\n })))));\n }\n const reply = () => {\n DiscussionControls.replyAction.call(this.attrs.discussion, true).catch(() => {});\n };\n return m(\"button\", {\n className: \"Post ReplyPlaceholder\",\n onclick: reply\n }, m(\"div\", {\n className: \"Post-container\"\n }, m(\"div\", {\n className: \"Post-side\"\n }, m(Avatar, {\n user: app.session.user,\n className: \"Post-avatar\"\n })), m(\"div\", {\n className: \"Post-main\"\n }, m(\"span\", {\n className: \"Post-header\"\n }, app.translator.trans('core.forum.post_stream.reply_placeholder')))));\n }\n anchorPreview(preview) {\n const anchorToBottom = $(window).scrollTop() + $(window).height() >= $(document).height();\n preview();\n if (anchorToBottom) {\n $(window).scrollTop($(document).height());\n }\n }\n}\nflarum.reg.add('core', 'forum/components/ReplyPlaceholder', ReplyPlaceholder);","import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport ScrollListener from '../../common/utils/ScrollListener';\nimport LoadingPost from './LoadingPost';\nimport ReplyPlaceholder from './ReplyPlaceholder';\nimport Button from '../../common/components/Button';\nimport ItemList from '../../common/utils/ItemList';\n\n/**\n * The `PostStream` component displays an infinitely-scrollable wall of posts in\n * a discussion. Posts that have not loaded will be displayed as placeholders.\n *\n * ### Attrs\n *\n * - `discussion`\n * - `stream`\n * - `targetPost`\n * - `onPositionChange`\n */\nexport default class PostStream extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.discussion = this.attrs.discussion;\n this.stream = this.attrs.stream;\n this.scrollListener = new ScrollListener(this.onscroll.bind(this));\n }\n view() {\n let lastTime;\n const viewingEnd = this.stream.viewingEnd();\n const posts = this.stream.posts();\n const postIds = this.discussion.postIds();\n const postFadeIn = vnode => {\n $(vnode.dom).addClass('fadeIn');\n // 500 is the duration of the fadeIn CSS animation + 100ms,\n // so the animation has time to complete\n setTimeout(() => $(vnode.dom).removeClass('fadeIn'), 500);\n };\n const items = posts.map((post, i) => {\n let content;\n const attrs = {\n 'data-index': this.stream.visibleStart + i\n };\n if (post) {\n const time = post.createdAt();\n const PostComponent = app.postComponents[post.contentType()];\n content = !!PostComponent && m(PostComponent, {\n post: post\n });\n attrs.key = 'post' + post.id();\n attrs.oncreate = postFadeIn;\n attrs['data-time'] = time.toISOString();\n attrs['data-number'] = post.number();\n attrs['data-id'] = post.id();\n attrs['data-type'] = post.contentType();\n\n // If the post before this one was more than 4 days ago, we will\n // display a 'time gap' indicating how long it has been in between\n // the posts.\n const dt = time - lastTime;\n if (dt > 1000 * 60 * 60 * 24 * 4) {\n content = [m(\"div\", {\n className: \"PostStream-timeGap\"\n }, m(\"span\", null, app.translator.trans('core.forum.post_stream.time_lapsed_text', {\n period: dayjs().add(dt, 'ms').fromNow(true)\n }))), content];\n }\n lastTime = time;\n } else {\n attrs.key = 'post' + postIds[this.stream.visibleStart + i];\n content = m(LoadingPost, null);\n }\n return m(\"div\", Object.assign({\n className: \"PostStream-item\"\n }, attrs), content);\n });\n if (!viewingEnd && posts[this.stream.visibleEnd - this.stream.visibleStart - 1]) {\n items.push(m(\"div\", {\n className: \"PostStream-loadMore\",\n key: \"loadMore\"\n }, m(Button, {\n className: \"Button\",\n onclick: this.stream.loadNext.bind(this.stream)\n }, app.translator.trans('core.forum.post_stream.load_more_button'))));\n }\n\n // Allow extensions to add items to the end of the post stream.\n if (viewingEnd) {\n items.push(...this.endItems().toArray());\n }\n\n // If we're viewing the end of the discussion, the user can reply, and\n // is not already doing so, then show a 'write a reply' placeholder.\n if (viewingEnd && (!app.session.user || this.discussion.canReply())) {\n items.push(m(\"div\", {\n className: \"PostStream-item\",\n key: \"reply\",\n \"data-index\": this.stream.count(),\n oncreate: postFadeIn\n }, m(ReplyPlaceholder, {\n discussion: this.discussion\n })));\n }\n return m(\"div\", {\n className: \"PostStream\",\n role: \"feed\",\n \"aria-live\": \"off\",\n \"aria-busy\": this.stream.pagesLoading ? 'true' : 'false'\n }, items);\n }\n\n /**\n * @returns {ItemList}\n */\n endItems() {\n const items = new ItemList();\n return items;\n }\n onupdate(vnode) {\n super.onupdate(vnode);\n this.triggerScroll();\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.triggerScroll();\n\n // This is wrapped in setTimeout due to the following Mithril issue:\n // https://github.com/lhorie/mithril.js/issues/637\n setTimeout(() => this.scrollListener.start());\n }\n onremove(vnode) {\n super.onremove(vnode);\n this.scrollListener.stop();\n clearTimeout(this.calculatePositionTimeout);\n }\n\n /**\n * Start scrolling, if appropriate, to a newly-targeted post.\n */\n triggerScroll() {\n if (!this.stream.needsScroll) return;\n const target = this.stream.targetPost;\n this.stream.needsScroll = false;\n if ('number' in target) {\n this.scrollToNumber(target.number, this.stream.animateScroll);\n } else if ('index' in target) {\n this.scrollToIndex(target.index, this.stream.animateScroll, target.reply);\n }\n }\n\n /**\n *\n * @param {number} top\n */\n onscroll(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n if (this.stream.paused || this.stream.pagesLoading) return;\n this.updateScrubber(top);\n this.loadPostsIfNeeded(top);\n\n // Throttle calculation of our position (start/end numbers of posts in the\n // viewport) to 100ms.\n clearTimeout(this.calculatePositionTimeout);\n this.calculatePositionTimeout = setTimeout(this.calculatePosition.bind(this, top), 100);\n }\n\n /**\n * Check if either extreme of the post stream is in the viewport,\n * and if so, trigger loading the next/previous page.\n *\n * @param {number} top\n */\n loadPostsIfNeeded(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n const marginTop = this.getMarginTop();\n const viewportHeight = $(window).height() - marginTop;\n const viewportTop = top + marginTop;\n const loadAheadDistance = 300;\n if (this.stream.visibleStart > 0) {\n const $item = this.$('.PostStream-item[data-index=' + this.stream.visibleStart + ']');\n if ($item.length && $item.offset().top > viewportTop - loadAheadDistance) {\n this.stream.loadPrevious();\n }\n }\n if (this.stream.visibleEnd < this.stream.count()) {\n const $item = this.$('.PostStream-item[data-index=' + (this.stream.visibleEnd - 1) + ']');\n if ($item.length && $item.offset().top + $item.outerHeight(true) < viewportTop + viewportHeight + loadAheadDistance) {\n this.stream.loadNext();\n }\n }\n }\n updateScrubber(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n const marginTop = this.getMarginTop();\n const viewportHeight = $(window).height() - marginTop;\n const viewportTop = top + marginTop;\n\n // Before looping through all of the posts, we reset the scrollbar\n // properties to a 'default' state. These values reflect what would be\n // seen if the browser were scrolled right up to the top of the page,\n // and the viewport had a height of 0.\n const $items = this.$('.PostStream-item[data-index]');\n let visible = 0;\n let period = '';\n let indexFromViewPort = null;\n\n // Now loop through each of the items in the discussion. An 'item' is\n // either a single post or a 'gap' of one or more posts that haven't\n // been loaded yet.\n $items.each(function () {\n const $this = $(this);\n const top = $this.offset().top;\n const height = $this.outerHeight(true);\n\n // If this item is above the top of the viewport, skip to the next\n // one. If it's below the bottom of the viewport, break out of the\n // loop.\n if (top + height < viewportTop) {\n return true;\n }\n if (top > viewportTop + viewportHeight) {\n return false;\n }\n\n // Work out how many pixels of this item are visible inside the viewport.\n // Then add the proportion of this item's total height to the index.\n const visibleTop = Math.max(0, viewportTop - top);\n const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);\n const visiblePost = visibleBottom - visibleTop;\n\n // We take the index of the first item that passed the previous checks.\n // It is the item that is first visible in the viewport.\n if (indexFromViewPort === null) {\n indexFromViewPort = parseFloat($this.data('index')) + visibleTop / height;\n }\n if (visiblePost > 0) {\n visible += visiblePost / height;\n }\n\n // If this item has a time associated with it, then set the\n // scrollbar's current period to a formatted version of this time.\n const time = $this.data('time');\n if (time) period = time;\n });\n\n // If indexFromViewPort is null, it means no posts are visible in the\n // viewport. This can happen, when drafting a long reply post. In that case\n // set the index to the last post.\n this.stream.index = indexFromViewPort !== null ? indexFromViewPort + 1 : this.stream.count();\n this.stream.visible = visible;\n if (period) this.stream.description = dayjs(period).format('MMMM YYYY');\n }\n\n /**\n * Work out which posts (by number) are currently visible in the viewport, and\n * fire an event with the information.\n */\n calculatePosition(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n const marginTop = this.getMarginTop();\n const $window = $(window);\n const viewportHeight = $window.height() - marginTop;\n const scrollTop = $window.scrollTop() + marginTop;\n const viewportTop = top + marginTop;\n let startNumber;\n let endNumber;\n this.$('.PostStream-item').each(function () {\n const $item = $(this);\n const top = $item.offset().top;\n const height = $item.outerHeight(true);\n const visibleTop = Math.max(0, viewportTop - top);\n const threeQuartersVisible = visibleTop / height < 0.75;\n const coversQuarterOfViewport = (height - visibleTop) / viewportHeight > 0.25;\n if (startNumber === undefined && (threeQuartersVisible || coversQuarterOfViewport)) {\n startNumber = $item.data('number');\n }\n if (top + height > scrollTop) {\n if (top + height < scrollTop + viewportHeight) {\n if ($item.data('number')) {\n endNumber = $item.data('number');\n }\n } else return false;\n }\n });\n if (startNumber) {\n this.attrs.onPositionChange(startNumber || 1, endNumber, startNumber);\n }\n }\n\n /**\n * Get the distance from the top of the viewport to the point at which we\n * would consider a post to be the first one visible.\n *\n * @return {number}\n */\n getMarginTop() {\n const headerId = app.screen() === 'phone' ? '#app-navigation' : '#header';\n return this.$() && $(headerId).outerHeight() + parseInt(this.$().css('margin-top'), 10);\n }\n\n /**\n * Scroll down to a certain post by number and 'flash' it.\n *\n * @param {number} number\n * @param {boolean} animate\n * @return {JQueryDeferred}\n */\n scrollToNumber(number, animate) {\n const $item = this.$(\".PostStream-item[data-number=\".concat(number, \"]\"));\n return this.scrollToItem($item, animate).then(this.flashItem.bind(this, $item));\n }\n\n /**\n * Scroll down to a certain post by index.\n *\n * @param {number} index\n * @param {boolean} animate\n * @param {boolean} reply Whether or not to scroll to the reply placeholder.\n * @return {JQueryDeferred}\n */\n scrollToIndex(index, animate, reply) {\n const $item = reply ? $('.PostStream-item:last-child') : this.$(\".PostStream-item[data-index=\".concat(index, \"]\"));\n this.scrollToItem($item, animate, true, reply);\n if (reply) {\n this.flashItem($item);\n }\n }\n\n /**\n * Scroll down to the given post.\n *\n * @param {JQuery} $item\n * @param {boolean} animate\n * @param {boolean} force Whether or not to force scrolling to the item, even\n * if it is already in the viewport.\n * @param {boolean} reply Whether or not to scroll to the reply placeholder.\n * @return {JQueryDeferred}\n */\n scrollToItem($item, animate, force, reply) {\n const $container = $('html, body').stop(true);\n const index = $item.data('index');\n if ($item.length) {\n const itemTop = $item.offset().top - this.getMarginTop();\n const itemBottom = $item.offset().top + $item.height();\n const scrollTop = $(document).scrollTop();\n const scrollBottom = scrollTop + $(window).height();\n\n // If the item is already in the viewport, we may not need to scroll.\n // If we're scrolling to the reply placeholder, we'll make sure its\n // bottom will line up with the top of the composer.\n if (force || itemTop < scrollTop || itemBottom > scrollBottom) {\n const top = reply ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;\n if (!animate) {\n $container.scrollTop(top);\n } else if (top !== scrollTop) {\n $container.animate({\n scrollTop: top\n }, 'fast');\n }\n }\n }\n const updateScrubberHeight = () => {\n // We manually set the index because we want to display the index of the\n // exact post we've scrolled to, not just that of the first post within viewport.\n this.updateScrubber();\n if (index !== undefined) this.stream.index = index + 1;\n };\n\n // If we don't update this before the scroll, the scrubber will start\n // at the top, and animate down, which can be confusing\n updateScrubberHeight();\n this.stream.forceUpdateScrubber = true;\n return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => {\n m.redraw.sync();\n\n // Rendering post contents will probably throw off our position.\n // To counter this, we'll scroll either:\n // - To the reply placeholder (aligned with composer top)\n // - To the top of the page if we're on the first post\n // - To the top of a post (if that post exists)\n // If the post does not currently exist, it's probably\n // outside of the range we loaded in, so we won't adjust anything,\n // as it will soon be rendered by the \"load more\" system.\n let itemOffset;\n if (reply) {\n const $placeholder = $('.PostStream-item:last-child');\n $(window).scrollTop($placeholder.offset().top + $placeholder.height() - $(window).height() + app.composer.computedHeight());\n } else if (index === 0) {\n $(window).scrollTop(0);\n } else if (itemOffset = $(\".PostStream-item[data-index=\".concat(index, \"]\")).offset()) {\n $(window).scrollTop(itemOffset.top - this.getMarginTop());\n }\n\n // We want to adjust this again after posts have been loaded in\n // and position adjusted so that the scrubber's height is accurate.\n updateScrubberHeight();\n this.calculatePosition();\n this.stream.paused = false;\n // Check if we need to load more posts after scrolling.\n this.loadPostsIfNeeded();\n });\n }\n\n /**\n * 'Flash' the given post, drawing the user's attention to it.\n *\n * @param {JQuery} $item\n */\n flashItem($item) {\n // This might execute before the fadeIn class has been removed in PostStreamItem's\n // oncreate, so we remove it just to be safe and avoid a double animation.\n $item.removeClass('fadeIn');\n $item.addClass('flash').on('animationend webkitAnimationEnd', e => {\n $item.removeClass('flash');\n });\n }\n}\nflarum.reg.add('core', 'forum/components/PostStream', PostStream);"],"names":["LoadingPost","Component","view","m","className","Avatar","user","flarum","reg","add","ReplyPlaceholder","app","this","attrs","discussion","username","listItems","toArray","ComposerPostPreview","composer","surround","anchorPreview","bind","onclick","DiscussionControls","catch","preview","anchorToBottom","$","window","scrollTop","height","document","PostStream","oninit","vnode","super","stream","scrollListener","ScrollListener","onscroll","lastTime","viewingEnd","posts","postIds","postFadeIn","dom","addClass","setTimeout","removeClass","items","map","post","i","content","visibleStart","time","createdAt","PostComponent","contentType","key","id","oncreate","toISOString","number","dt","period","dayjs","fromNow","Object","assign","visibleEnd","push","Button","loadNext","endItems","canReply","count","role","pagesLoading","ItemList","onupdate","triggerScroll","start","onremove","stop","clearTimeout","calculatePositionTimeout","needsScroll","target","targetPost","scrollToNumber","animateScroll","scrollToIndex","index","reply","top","pageYOffset","paused","updateScrubber","loadPostsIfNeeded","calculatePosition","marginTop","getMarginTop","viewportHeight","viewportTop","$item","length","offset","loadPrevious","outerHeight","$items","visible","indexFromViewPort","each","$this","visibleTop","Math","max","visiblePost","min","parseFloat","data","description","format","$window","startNumber","endNumber","undefined","onPositionChange","headerId","parseInt","css","animate","concat","scrollToItem","then","flashItem","force","$container","itemTop","itemBottom","scrollBottom","is","updateScrubberHeight","forceUpdateScrubber","Promise","all","promise","loadPromise","itemOffset","redraw","sync","$placeholder","on","e"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/PostStream.js","mappings":";;;;;;;;;;;;;;;AAA+C;AACK;;AAEpD;AACA;AACA;AACA;AACe,0BAA0B,yDAAS;AAClD;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK,IAAI,iEAAM;AACf;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA;;;;;;;;;;;;;;;;;;;;;AC7BkC;AACa;AACgB;AACvB;AACU;AACE;AACD;;AAEnD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,yBAAyB,yDAAS;AACjD;AACA;AACA;AACA;AACA,8BAA8B,oEAAc;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,8BAA8B,iEAAkB;AAChD;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW,kBAAkB,mEAAoB;AACjD;AACA,WAAW;AACX;AACA;AACA,QAAQ;AACR;AACA,oBAAoB,oDAAW;AAC/B;AACA;AACA;AACA,OAAO;AACP,KAAK;AACL;AACA;AACA;AACA;AACA,OAAO,IAAI,iEAAM;AACjB;AACA;AACA,OAAO,EAAE,mEAAoB;AAC7B;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,wBAAwB,+DAAgB;AACxC;AACA;AACA;AACA;AACA;AACA,OAAO,IAAI,yDAAgB;AAC3B;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA,eAAe;AACf;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;;AAEA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,KAAK;;AAEL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,UAAU;AACV;AACA,KAAK;AACL;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA,qBAAqB,yDAAU;AAC/B;AACA;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB,aAAa,SAAS;AACtB,cAAc;AACd;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB,aAAa,SAAS;AACtB,aAAa,SAAS;AACtB,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB,aAAa,SAAS;AACtB,aAAa,SAAS;AACtB;AACA,aAAa,SAAS;AACtB,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,8DAA8D,0EAA2B;AACzF;AACA;AACA,UAAU;AACV;AACA;AACA,WAAW;AACX;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qGAAqG,0EAA2B;AAChI,QAAQ;AACR;AACA,QAAQ;AACR;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;;;;;;;;;;;;;;;;;;;;;ACxakC;AACa;AACM;AACQ;AACL;AACD;AACH;;AAEpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,+BAA+B,yDAAS;AACvD;AACA,QAAQ,4EAA6B;AACrC;AACA;AACA;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA,OAAO,IAAI,iEAAM;AACjB,cAAc,+DAAgB;AAC9B;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA,OAAO,EAAE,oEAAQ,CAAC,+DAAgB;AAClC;AACA,OAAO,EAAE,qEAAS,CAAC,sEAAuB;AAC1C;AACA,OAAO,IAAI,4DAAmB;AAC9B;AACA,kBAAkB,2DAAY;AAC9B;AACA,OAAO;AACP;AACA;AACA,MAAM,kFAAmC,4CAA4C;AACrF;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK,IAAI,iEAAM;AACf,YAAY,+DAAgB;AAC5B;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/LoadingPost.js","webpack://@flarum/core/./src/forum/components/PostStream.js","webpack://@flarum/core/./src/forum/components/ReplyPlaceholder.js"],"sourcesContent":["import Component from '../../common/Component';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `LoadingPost` component shows a placeholder that looks like a post,\n * indicating that the post is loading.\n */\nexport default class LoadingPost extends Component {\n view() {\n return m(\"div\", {\n className: \"Post CommentPost LoadingPost\"\n }, m(\"header\", {\n className: \"Post-header\"\n }, m(Avatar, {\n user: null,\n className: \"PostUser-avatar\"\n }), m(\"div\", {\n className: \"fakeText\"\n })), m(\"div\", {\n className: \"Post-body\"\n }, m(\"div\", {\n className: \"fakeText\"\n }), m(\"div\", {\n className: \"fakeText\"\n }), m(\"div\", {\n className: \"fakeText\"\n })));\n }\n}\nflarum.reg.add('core', 'forum/components/LoadingPost', LoadingPost);","import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport ScrollListener from '../../common/utils/ScrollListener';\nimport LoadingPost from './LoadingPost';\nimport ReplyPlaceholder from './ReplyPlaceholder';\nimport Button from '../../common/components/Button';\nimport ItemList from '../../common/utils/ItemList';\n\n/**\n * The `PostStream` component displays an infinitely-scrollable wall of posts in\n * a discussion. Posts that have not loaded will be displayed as placeholders.\n *\n * ### Attrs\n *\n * - `discussion`\n * - `stream`\n * - `targetPost`\n * - `onPositionChange`\n */\nexport default class PostStream extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.discussion = this.attrs.discussion;\n this.stream = this.attrs.stream;\n this.scrollListener = new ScrollListener(this.onscroll.bind(this));\n }\n view() {\n let lastTime;\n const viewingEnd = this.stream.viewingEnd();\n const posts = this.stream.posts();\n const postIds = this.discussion.postIds();\n const postFadeIn = vnode => {\n $(vnode.dom).addClass('fadeIn');\n // 500 is the duration of the fadeIn CSS animation + 100ms,\n // so the animation has time to complete\n setTimeout(() => $(vnode.dom).removeClass('fadeIn'), 500);\n };\n const items = posts.map((post, i) => {\n let content;\n const attrs = {\n 'data-index': this.stream.visibleStart + i\n };\n if (post) {\n const time = post.createdAt();\n const PostComponent = app.postComponents[post.contentType()];\n content = !!PostComponent && m(PostComponent, {\n post: post\n });\n attrs.key = 'post' + post.id();\n attrs.oncreate = postFadeIn;\n attrs['data-time'] = time.toISOString();\n attrs['data-number'] = post.number();\n attrs['data-id'] = post.id();\n attrs['data-type'] = post.contentType();\n\n // If the post before this one was more than 4 days ago, we will\n // display a 'time gap' indicating how long it has been in between\n // the posts.\n const dt = time - lastTime;\n if (dt > 1000 * 60 * 60 * 24 * 4) {\n content = [m(\"div\", {\n className: \"PostStream-timeGap\"\n }, m(\"span\", null, app.translator.trans('core.forum.post_stream.time_lapsed_text', {\n period: dayjs().add(dt, 'ms').fromNow(true)\n }))), content];\n }\n lastTime = time;\n } else {\n attrs.key = 'post' + postIds[this.stream.visibleStart + i];\n content = m(LoadingPost, null);\n }\n return m(\"div\", Object.assign({\n className: \"PostStream-item\"\n }, attrs), content);\n });\n if (!viewingEnd && posts[this.stream.visibleEnd - this.stream.visibleStart - 1]) {\n items.push(m(\"div\", {\n className: \"PostStream-loadMore\",\n key: \"loadMore\"\n }, m(Button, {\n className: \"Button\",\n onclick: this.stream.loadNext.bind(this.stream)\n }, app.translator.trans('core.forum.post_stream.load_more_button'))));\n }\n\n // Allow extensions to add items to the end of the post stream.\n if (viewingEnd) {\n items.push(...this.endItems().toArray());\n }\n\n // If we're viewing the end of the discussion, the user can reply, and\n // is not already doing so, then show a 'write a reply' placeholder.\n if (viewingEnd && (!app.session.user || this.discussion.canReply())) {\n items.push(m(\"div\", {\n className: \"PostStream-item\",\n key: \"reply\",\n \"data-index\": this.stream.count(),\n oncreate: postFadeIn\n }, m(ReplyPlaceholder, {\n discussion: this.discussion\n })));\n }\n return m(\"div\", {\n className: \"PostStream\",\n role: \"feed\",\n \"aria-live\": \"off\",\n \"aria-busy\": this.stream.pagesLoading ? 'true' : 'false'\n }, items);\n }\n\n /**\n * @returns {ItemList}\n */\n endItems() {\n const items = new ItemList();\n return items;\n }\n onupdate(vnode) {\n super.onupdate(vnode);\n this.triggerScroll();\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.triggerScroll();\n\n // This is wrapped in setTimeout due to the following Mithril issue:\n // https://github.com/lhorie/mithril.js/issues/637\n setTimeout(() => this.scrollListener.start());\n }\n onremove(vnode) {\n super.onremove(vnode);\n this.scrollListener.stop();\n clearTimeout(this.calculatePositionTimeout);\n }\n\n /**\n * Start scrolling, if appropriate, to a newly-targeted post.\n */\n triggerScroll() {\n if (!this.stream.needsScroll) return;\n const target = this.stream.targetPost;\n this.stream.needsScroll = false;\n if ('number' in target) {\n this.scrollToNumber(target.number, this.stream.animateScroll);\n } else if ('index' in target) {\n this.scrollToIndex(target.index, this.stream.animateScroll, target.reply);\n }\n }\n\n /**\n *\n * @param {number} top\n */\n onscroll(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n if (this.stream.paused || this.stream.pagesLoading) return;\n this.updateScrubber(top);\n this.loadPostsIfNeeded(top);\n\n // Throttle calculation of our position (start/end numbers of posts in the\n // viewport) to 100ms.\n clearTimeout(this.calculatePositionTimeout);\n this.calculatePositionTimeout = setTimeout(this.calculatePosition.bind(this, top), 100);\n }\n\n /**\n * Check if either extreme of the post stream is in the viewport,\n * and if so, trigger loading the next/previous page.\n *\n * @param {number} top\n */\n loadPostsIfNeeded(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n const marginTop = this.getMarginTop();\n const viewportHeight = $(window).height() - marginTop;\n const viewportTop = top + marginTop;\n const loadAheadDistance = 300;\n if (this.stream.visibleStart > 0) {\n const $item = this.$('.PostStream-item[data-index=' + this.stream.visibleStart + ']');\n if ($item.length && $item.offset().top > viewportTop - loadAheadDistance) {\n this.stream.loadPrevious();\n }\n }\n if (this.stream.visibleEnd < this.stream.count()) {\n const $item = this.$('.PostStream-item[data-index=' + (this.stream.visibleEnd - 1) + ']');\n if ($item.length && $item.offset().top + $item.outerHeight(true) < viewportTop + viewportHeight + loadAheadDistance) {\n this.stream.loadNext();\n }\n }\n }\n updateScrubber(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n const marginTop = this.getMarginTop();\n const viewportHeight = $(window).height() - marginTop;\n const viewportTop = top + marginTop;\n\n // Before looping through all of the posts, we reset the scrollbar\n // properties to a 'default' state. These values reflect what would be\n // seen if the browser were scrolled right up to the top of the page,\n // and the viewport had a height of 0.\n const $items = this.$('.PostStream-item[data-index]');\n let visible = 0;\n let period = '';\n let indexFromViewPort = null;\n\n // Now loop through each of the items in the discussion. An 'item' is\n // either a single post or a 'gap' of one or more posts that haven't\n // been loaded yet.\n $items.each(function () {\n const $this = $(this);\n const top = $this.offset().top;\n const height = $this.outerHeight(true);\n\n // If this item is above the top of the viewport, skip to the next\n // one. If it's below the bottom of the viewport, break out of the\n // loop.\n if (top + height < viewportTop) {\n return true;\n }\n if (top > viewportTop + viewportHeight) {\n return false;\n }\n\n // Work out how many pixels of this item are visible inside the viewport.\n // Then add the proportion of this item's total height to the index.\n const visibleTop = Math.max(0, viewportTop - top);\n const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);\n const visiblePost = visibleBottom - visibleTop;\n\n // We take the index of the first item that passed the previous checks.\n // It is the item that is first visible in the viewport.\n if (indexFromViewPort === null) {\n indexFromViewPort = parseFloat($this.data('index')) + visibleTop / height;\n }\n if (visiblePost > 0) {\n visible += visiblePost / height;\n }\n\n // If this item has a time associated with it, then set the\n // scrollbar's current period to a formatted version of this time.\n const time = $this.data('time');\n if (time) period = time;\n });\n\n // If indexFromViewPort is null, it means no posts are visible in the\n // viewport. This can happen, when drafting a long reply post. In that case\n // set the index to the last post.\n this.stream.index = indexFromViewPort !== null ? indexFromViewPort + 1 : this.stream.count();\n this.stream.visible = visible;\n if (period) this.stream.description = dayjs(period).format('MMMM YYYY');\n }\n\n /**\n * Work out which posts (by number) are currently visible in the viewport, and\n * fire an event with the information.\n */\n calculatePosition(top) {\n if (top === void 0) {\n top = window.pageYOffset;\n }\n const marginTop = this.getMarginTop();\n const $window = $(window);\n const viewportHeight = $window.height() - marginTop;\n const scrollTop = $window.scrollTop() + marginTop;\n const viewportTop = top + marginTop;\n let startNumber;\n let endNumber;\n this.$('.PostStream-item').each(function () {\n const $item = $(this);\n const top = $item.offset().top;\n const height = $item.outerHeight(true);\n const visibleTop = Math.max(0, viewportTop - top);\n const threeQuartersVisible = visibleTop / height < 0.75;\n const coversQuarterOfViewport = (height - visibleTop) / viewportHeight > 0.25;\n if (startNumber === undefined && (threeQuartersVisible || coversQuarterOfViewport)) {\n startNumber = $item.data('number');\n }\n if (top + height > scrollTop) {\n if (top + height < scrollTop + viewportHeight) {\n if ($item.data('number')) {\n endNumber = $item.data('number');\n }\n } else return false;\n }\n });\n if (startNumber) {\n this.attrs.onPositionChange(startNumber || 1, endNumber, startNumber);\n }\n }\n\n /**\n * Get the distance from the top of the viewport to the point at which we\n * would consider a post to be the first one visible.\n *\n * @return {number}\n */\n getMarginTop() {\n const headerId = app.screen() === 'phone' ? '#app-navigation' : '#header';\n return this.$() && $(headerId).outerHeight() + parseInt(this.$().css('margin-top'), 10);\n }\n\n /**\n * Scroll down to a certain post by number and 'flash' it.\n *\n * @param {number} number\n * @param {boolean} animate\n * @return {JQueryDeferred}\n */\n scrollToNumber(number, animate) {\n const $item = this.$(\".PostStream-item[data-number=\".concat(number, \"]\"));\n return this.scrollToItem($item, animate).then(this.flashItem.bind(this, $item));\n }\n\n /**\n * Scroll down to a certain post by index.\n *\n * @param {number} index\n * @param {boolean} animate\n * @param {boolean} reply Whether or not to scroll to the reply placeholder.\n * @return {JQueryDeferred}\n */\n scrollToIndex(index, animate, reply) {\n const $item = reply ? $('.PostStream-item:last-child') : this.$(\".PostStream-item[data-index=\".concat(index, \"]\"));\n this.scrollToItem($item, animate, true, reply);\n if (reply) {\n this.flashItem($item);\n }\n }\n\n /**\n * Scroll down to the given post.\n *\n * @param {JQuery} $item\n * @param {boolean} animate\n * @param {boolean} force Whether or not to force scrolling to the item, even\n * if it is already in the viewport.\n * @param {boolean} reply Whether or not to scroll to the reply placeholder.\n * @return {JQueryDeferred}\n */\n scrollToItem($item, animate, force, reply) {\n const $container = $('html, body').stop(true);\n const index = $item.data('index');\n if ($item.length) {\n const itemTop = $item.offset().top - this.getMarginTop();\n const itemBottom = $item.offset().top + $item.height();\n const scrollTop = $(document).scrollTop();\n const scrollBottom = scrollTop + $(window).height();\n\n // If the item is already in the viewport, we may not need to scroll.\n // If we're scrolling to the reply placeholder, we'll make sure its\n // bottom will line up with the top of the composer.\n if (force || itemTop < scrollTop || itemBottom > scrollBottom) {\n const top = reply ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;\n if (!animate) {\n $container.scrollTop(top);\n } else if (top !== scrollTop) {\n $container.animate({\n scrollTop: top\n }, 'fast');\n }\n }\n }\n const updateScrubberHeight = () => {\n // We manually set the index because we want to display the index of the\n // exact post we've scrolled to, not just that of the first post within viewport.\n this.updateScrubber();\n if (index !== undefined) this.stream.index = index + 1;\n };\n\n // If we don't update this before the scroll, the scrubber will start\n // at the top, and animate down, which can be confusing\n updateScrubberHeight();\n this.stream.forceUpdateScrubber = true;\n return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => {\n m.redraw.sync();\n\n // Rendering post contents will probably throw off our position.\n // To counter this, we'll scroll either:\n // - To the reply placeholder (aligned with composer top)\n // - To the top of the page if we're on the first post\n // - To the top of a post (if that post exists)\n // If the post does not currently exist, it's probably\n // outside of the range we loaded in, so we won't adjust anything,\n // as it will soon be rendered by the \"load more\" system.\n let itemOffset;\n if (reply) {\n const $placeholder = $('.PostStream-item:last-child');\n $(window).scrollTop($placeholder.offset().top + $placeholder.height() - $(window).height() + app.composer.computedHeight());\n } else if (index === 0) {\n $(window).scrollTop(0);\n } else if (itemOffset = $(\".PostStream-item[data-index=\".concat(index, \"]\")).offset()) {\n $(window).scrollTop(itemOffset.top - this.getMarginTop());\n }\n\n // We want to adjust this again after posts have been loaded in\n // and position adjusted so that the scrubber's height is accurate.\n updateScrubberHeight();\n this.calculatePosition();\n this.stream.paused = false;\n // Check if we need to load more posts after scrolling.\n this.loadPostsIfNeeded();\n });\n }\n\n /**\n * 'Flash' the given post, drawing the user's attention to it.\n *\n * @param {JQuery} $item\n */\n flashItem($item) {\n // This might execute before the fadeIn class has been removed in PostStreamItem's\n // oncreate, so we remove it just to be safe and avoid a double animation.\n $item.removeClass('fadeIn');\n $item.addClass('flash').on('animationend webkitAnimationEnd', e => {\n $item.removeClass('flash');\n });\n }\n}\nflarum.reg.add('core', 'forum/components/PostStream', PostStream);","import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport username from '../../common/helpers/username';\nimport DiscussionControls from '../utils/DiscussionControls';\nimport ComposerPostPreview from './ComposerPostPreview';\nimport listItems from '../../common/helpers/listItems';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ReplyPlaceholder` component displays a placeholder for a reply, which,\n * when clicked, opens the reply composer.\n *\n * ### Attrs\n *\n * - `discussion`\n */\nexport default class ReplyPlaceholder extends Component {\n view() {\n if (app.composer.composingReplyTo(this.attrs.discussion)) {\n return m(\"article\", {\n className: \"Post CommentPost editing\",\n \"aria-busy\": \"true\"\n }, m(\"div\", {\n className: \"Post-container\"\n }, m(\"div\", {\n className: \"Post-side\"\n }, m(Avatar, {\n user: app.session.user,\n className: \"Post-avatar\"\n })), m(\"div\", {\n className: \"Post-main\"\n }, m(\"header\", {\n className: \"Post-header\"\n }, m(\"div\", {\n className: \"PostUser\"\n }, m(\"h3\", {\n className: \"PostUser-name\"\n }, username(app.session.user)), m(\"ul\", {\n className: \"PostUser-badges badges badges--packed\"\n }, listItems(app.session.user.badges().toArray())))), m(\"div\", {\n className: \"Post-body\"\n }, m(ComposerPostPreview, {\n className: \"Post-body\",\n composer: app.composer,\n surround: this.anchorPreview.bind(this)\n })))));\n }\n const reply = () => {\n DiscussionControls.replyAction.call(this.attrs.discussion, true).catch(() => {});\n };\n return m(\"button\", {\n className: \"Post ReplyPlaceholder\",\n onclick: reply\n }, m(\"div\", {\n className: \"Post-container\"\n }, m(\"div\", {\n className: \"Post-side\"\n }, m(Avatar, {\n user: app.session.user,\n className: \"Post-avatar\"\n })), m(\"div\", {\n className: \"Post-main\"\n }, m(\"span\", {\n className: \"Post-header\"\n }, app.translator.trans('core.forum.post_stream.reply_placeholder')))));\n }\n anchorPreview(preview) {\n const anchorToBottom = $(window).scrollTop() + $(window).height() >= $(document).height();\n preview();\n if (anchorToBottom) {\n $(window).scrollTop($(document).height());\n }\n }\n}\nflarum.reg.add('core', 'forum/components/ReplyPlaceholder', ReplyPlaceholder);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/PostStreamScrubber.js b/framework/core/js/dist/forum/components/PostStreamScrubber.js index 24129d49b13..fe6fb7fb807 100644 --- a/framework/core/js/dist/forum/components/PostStreamScrubber.js +++ b/framework/core/js/dist/forum/components/PostStreamScrubber.js @@ -1,2 +1,339 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[808],{9816:(e,t,s)=>{s.r(t),s.d(t,{default:()=>h});var r=s(6789),o=s(2190),i=s(6917),a=s(4718),n=s(9133);class h extends o.Z{oninit(e){super.oninit(e),this.stream=this.attrs.stream,this.handlers={},this.scrollListener=new a.Z(this.updateScrubberValues.bind(this,{fromScroll:!0,forceHeightChange:!0}))}view(){const e=this.stream.count(),t=r.Z.translator.trans("core.forum.post_scrubber.viewing_text",{count:e,index:m("span",{className:"Scrubber-index"}),formattedCount:m("span",{className:"Scrubber-count"},(0,i.Z)(e))}),s=this.stream.discussion.unreadCount(),o=e?Math.min(e-this.stream.index,s)/e:0;function a(e){const t=$(e.dom),s={top:100-100*o+"%",height:100*o+"%",opacity:o?1:0};e.state.oldStyle?t.stop(!0).css(e.state.oldStyle).animate(s):t.css(s),e.state.oldStyle=s}const h=["PostStreamScrubber","Dropdown"];return this.attrs.className&&h.push(this.attrs.className),m("div",{className:h.join(" ")},m("button",{className:"Button Dropdown-toggle","data-toggle":"dropdown"},t," ",m(n.Z,{name:"fas fa-sort"})),m("div",{className:"Dropdown-menu dropdown-menu"},m("div",{className:"Scrubber"},m("a",{className:"Scrubber-first",onclick:this.goToFirst.bind(this)},m(n.Z,{name:"fas fa-angle-double-up"})," ",r.Z.translator.trans("core.forum.post_scrubber.original_post_link")),m("div",{className:"Scrubber-scrollbar"},m("div",{className:"Scrubber-before"}),m("div",{className:"Scrubber-handle"},m("div",{className:"Scrubber-bar"}),m("div",{className:"Scrubber-info"},m("strong",null,t),m("span",{className:"Scrubber-description"}))),m("div",{className:"Scrubber-after"}),m("div",{className:"Scrubber-unread",oncreate:a,onupdate:a},r.Z.translator.trans("core.forum.post_scrubber.unread_text",{count:s}))),m("a",{className:"Scrubber-last",onclick:this.goToLast.bind(this)},m(n.Z,{name:"fas fa-angle-double-down"})," ",r.Z.translator.trans("core.forum.post_scrubber.now_link")))))}onupdate(e){super.onupdate(e),this.stream.forceUpdateScrubber&&(this.stream.forceUpdateScrubber=!1,this.stream.loadPromise.then((()=>this.updateScrubberValues({animate:!0,forceHeightChange:!0}))))}oncreate(e){super.oncreate(e),$(window).on("resize",this.handlers.onresize=this.onresize.bind(this)).resize(),this.$(".Scrubber-scrollbar").bind("click",this.onclick.bind(this)).bind("dragstart mousedown touchstart",(e=>e.preventDefault())),this.dragging=!1,this.mouseStart=0,this.indexStart=0,this.$(".Scrubber-handle").bind("mousedown touchstart",this.onmousedown.bind(this)).click((e=>e.stopPropagation())),$(document).on("mousemove touchmove",this.handlers.onmousemove=this.onmousemove.bind(this)).on("mouseup touchend",this.handlers.onmouseup=this.onmouseup.bind(this)),setTimeout((()=>this.scrollListener.start())),this.stream.loadPromise.then((()=>this.updateScrubberValues({animate:!1,forceHeightChange:!0})))}onremove(e){super.onremove(e),this.scrollListener.stop(),$(window).off("resize",this.handlers.onresize),$(document).off("mousemove touchmove",this.handlers.onmousemove).off("mouseup touchend",this.handlers.onmouseup)}updateScrubberValues(e){void 0===e&&(e={});const t=this.stream.index,s=this.stream.count(),r=this.stream.visible||1,o=this.percentPerPost(),a=this.$();a.find(".Scrubber-index").text((0,i.Z)(this.stream.sanitizeIndex(Math.max(1,t)))),a.find(".Scrubber-description").text(this.stream.description),a.toggleClass("disabled",this.stream.disabled());const n={};if(n.before=Math.max(0,o.index*Math.min(t-1,s-r)),n.handle=Math.min(100-n.before,o.visible*r),n.after=100-n.before-n.handle,e.fromScroll&&this.stream.paused||this.adjustingHeight&&!e.forceHeightChange)return;const h=e.animate?"animate":"css";this.adjustingHeight=!0;const c=[];for(const e in n){const t=a.find(".Scrubber-".concat(e));c.push(t.stop(!0,!0)[h]({height:n[e]+"%"},"fast").promise()),"animate"===h&&t.css("overflow","visible")}Promise.all(c).then((()=>this.adjustingHeight=!1))}goToFirst(){this.stream.goToFirst(),this.updateScrubberValues({animate:!0,forceHeightChange:!0})}goToLast(){this.stream.goToLast(),this.updateScrubberValues({animate:!0,forceHeightChange:!0})}onresize(){const e=this.$(),t=this.$(".Scrubber-scrollbar");t.css("max-height",$(window).height()-e.offset().top+$(window).scrollTop()-parseInt($("#app").css("padding-bottom"),10)-(e.outerHeight()-t.outerHeight()))}onmousedown(e){e.redraw=!1,this.mouseStart=e.clientY||e.originalEvent.touches[0].clientY,this.indexStart=this.stream.index,this.dragging=!0,$("body").css("cursor","move"),this.$().toggleClass("dragging",this.dragging)}onmousemove(e){if(!this.dragging)return;const t=((e.clientY||e.originalEvent.touches[0].clientY)-this.mouseStart)/this.$(".Scrubber-scrollbar").outerHeight()*100/this.percentPerPost().index||0,s=Math.min(this.indexStart+t,this.stream.count()-1);this.stream.index=Math.max(0,s),this.updateScrubberValues()}onmouseup(){if(this.$().toggleClass("dragging",this.dragging),!this.dragging)return;this.mouseStart=0,this.indexStart=0,this.dragging=!1,$("body").css("cursor",""),this.$().removeClass("open");const e=Math.floor(this.stream.index);this.stream.goToIndex(e)}onclick(e){const t=this.$(".Scrubber-scrollbar");let s=((e.pageY||e.originalEvent.touches[0].pageY)-t.offset().top+$("body").scrollTop())/t.outerHeight()*100;s-=parseFloat(t.find(".Scrubber-handle")[0].style.height)/2;let r=s/this.percentPerPost().index;r=Math.max(0,Math.min(this.stream.count()-1,r)),this.stream.goToIndex(Math.floor(r)),this.updateScrubberValues({animate:!0,forceHeightChange:!0}),this.$().removeClass("open")}percentPerPost(){const e=this.stream.count()||1,t=this.stream.visible||1,s=50/this.$(".Scrubber-scrollbar").outerHeight()*100,r=Math.max(100/e,s/t);return{index:e===t?0:(100-r*t)/(e-t),visible:r}}}flarum.reg.add("core","forum/components/PostStreamScrubber",h)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/PostStreamScrubber"],{ + +/***/ "./src/forum/components/PostStreamScrubber.js": +/*!****************************************************!*\ + !*** ./src/forum/components/PostStreamScrubber.js ***! + \****************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ PostStreamScrubber) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_utils_formatNumber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/utils/formatNumber */ "./src/common/utils/formatNumber.ts"); +/* harmony import */ var _common_utils_ScrollListener__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/ScrollListener */ "./src/common/utils/ScrollListener.js"); +/* harmony import */ var _common_components_Icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/components/Icon */ "./src/common/components/Icon.tsx"); + + + + + + +/** + * The `PostStreamScrubber` component displays a scrubber which can be used to + * navigate/scrub through a post stream. + * + * ### Attrs + * + * - `stream` + * - `className` + */ +class PostStreamScrubber extends _common_Component__WEBPACK_IMPORTED_MODULE_1__["default"] { + oninit(vnode) { + super.oninit(vnode); + this.stream = this.attrs.stream; + this.handlers = {}; + this.scrollListener = new _common_utils_ScrollListener__WEBPACK_IMPORTED_MODULE_3__["default"](this.updateScrubberValues.bind(this, { + fromScroll: true, + forceHeightChange: true + })); + } + view() { + const count = this.stream.count(); + + // Index is left blank for performance reasons, it is filled in in updateScubberValues + const viewing = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.post_scrubber.viewing_text', { + count, + index: m("span", { + className: "Scrubber-index" + }), + formattedCount: m("span", { + className: "Scrubber-count" + }, (0,_common_utils_formatNumber__WEBPACK_IMPORTED_MODULE_2__["default"])(count)) + }); + const unreadCount = this.stream.discussion.unreadCount(); + const unreadPercent = count ? Math.min(count - this.stream.index, unreadCount) / count : 0; + function styleUnread(vnode) { + const $element = $(vnode.dom); + const newStyle = { + top: 100 - unreadPercent * 100 + '%', + height: unreadPercent * 100 + '%', + opacity: unreadPercent ? 1 : 0 + }; + if (vnode.state.oldStyle) { + $element.stop(true).css(vnode.state.oldStyle).animate(newStyle); + } else { + $element.css(newStyle); + } + vnode.state.oldStyle = newStyle; + } + const classNames = ['PostStreamScrubber', 'Dropdown']; + if (this.attrs.className) classNames.push(this.attrs.className); + return m("div", { + className: classNames.join(' ') + }, m("button", { + className: "Button Dropdown-toggle", + "data-toggle": "dropdown" + }, viewing, " ", m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_4__["default"], { + name: 'fas fa-sort' + })), m("div", { + className: "Dropdown-menu dropdown-menu" + }, m("div", { + className: "Scrubber" + }, m("a", { + className: "Scrubber-first", + onclick: this.goToFirst.bind(this) + }, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_4__["default"], { + name: 'fas fa-angle-double-up' + }), " ", _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.post_scrubber.original_post_link')), m("div", { + className: "Scrubber-scrollbar" + }, m("div", { + className: "Scrubber-before" + }), m("div", { + className: "Scrubber-handle" + }, m("div", { + className: "Scrubber-bar" + }), m("div", { + className: "Scrubber-info" + }, m("strong", null, viewing), m("span", { + className: "Scrubber-description" + }))), m("div", { + className: "Scrubber-after" + }), m("div", { + className: "Scrubber-unread", + oncreate: styleUnread, + onupdate: styleUnread + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.post_scrubber.unread_text', { + count: unreadCount + }))), m("a", { + className: "Scrubber-last", + onclick: this.goToLast.bind(this) + }, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_4__["default"], { + name: 'fas fa-angle-double-down' + }), " ", _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.post_scrubber.now_link'))))); + } + onupdate(vnode) { + super.onupdate(vnode); + if (this.stream.forceUpdateScrubber) { + this.stream.forceUpdateScrubber = false; + this.stream.loadPromise.then(() => this.updateScrubberValues({ + animate: true, + forceHeightChange: true + })); + } + } + oncreate(vnode) { + super.oncreate(vnode); + + // Whenever the window is resized, adjust the height of the scrollbar + // so that it fills the height of the sidebar. + $(window).on('resize', this.handlers.onresize = this.onresize.bind(this)).resize(); + + // When any part of the whole scrollbar is clicked, we want to jump to + // that position. + this.$('.Scrubber-scrollbar').bind('click', this.onclick.bind(this)) + + // Now we want to make the scrollbar handle draggable. Let's start by + // preventing default browser events from messing things up. + .bind('dragstart mousedown touchstart', e => e.preventDefault()); + + // When the mouse is pressed on the scrollbar handle, we capture some + // information about its current position. We will store this + // information in an object and pass it on to the document's + // mousemove/mouseup events later. + this.dragging = false; + this.mouseStart = 0; + this.indexStart = 0; + this.$('.Scrubber-handle').bind('mousedown touchstart', this.onmousedown.bind(this)) + + // Exempt the scrollbar handle from the 'jump to' click event. + .click(e => e.stopPropagation()); + + // When the mouse moves and when it is released, we pass the + // information that we captured when the mouse was first pressed onto + // some event handlers. These handlers will move the scrollbar/stream- + // content as appropriate. + $(document).on('mousemove touchmove', this.handlers.onmousemove = this.onmousemove.bind(this)).on('mouseup touchend', this.handlers.onmouseup = this.onmouseup.bind(this)); + setTimeout(() => this.scrollListener.start()); + this.stream.loadPromise.then(() => this.updateScrubberValues({ + animate: false, + forceHeightChange: true + })); + } + onremove(vnode) { + super.onremove(vnode); + this.scrollListener.stop(); + $(window).off('resize', this.handlers.onresize); + $(document).off('mousemove touchmove', this.handlers.onmousemove).off('mouseup touchend', this.handlers.onmouseup); + } + + /** + * Update the scrollbar's position to reflect the current values of the + * index/visible properties. + * + * @param {Partial<{fromScroll: boolean, forceHeightChange: boolean, animate: boolean}>} options + */ + updateScrubberValues(options) { + if (options === void 0) { + options = {}; + } + const index = this.stream.index; + const count = this.stream.count(); + const visible = this.stream.visible || 1; + const percentPerPost = this.percentPerPost(); + const $scrubber = this.$(); + $scrubber.find('.Scrubber-index').text((0,_common_utils_formatNumber__WEBPACK_IMPORTED_MODULE_2__["default"])(this.stream.sanitizeIndex(Math.max(1, index)))); + $scrubber.find('.Scrubber-description').text(this.stream.description); + $scrubber.toggleClass('disabled', this.stream.disabled()); + const heights = {}; + heights.before = Math.max(0, percentPerPost.index * Math.min(index - 1, count - visible)); + heights.handle = Math.min(100 - heights.before, percentPerPost.visible * visible); + heights.after = 100 - heights.before - heights.handle; + + // If the stream is paused, don't change height on scroll, as the viewport is being scrolled by the JS + // If a height change animation is already in progress, don't adjust height unless overriden + if (options.fromScroll && this.stream.paused || this.adjustingHeight && !options.forceHeightChange) return; + const func = options.animate ? 'animate' : 'css'; + this.adjustingHeight = true; + const animationPromises = []; + for (const part in heights) { + const $part = $scrubber.find(".Scrubber-".concat(part)); + animationPromises.push($part.stop(true, true)[func]({ + height: heights[part] + '%' + }, 'fast').promise()); + + // jQuery likes to put overflow:hidden, but because the scrollbar handle + // has a negative margin-left, we need to override. + if (func === 'animate') $part.css('overflow', 'visible'); + } + Promise.all(animationPromises).then(() => this.adjustingHeight = false); + } + + /** + * Go to the first post in the discussion. + */ + goToFirst() { + this.stream.goToFirst(); + this.updateScrubberValues({ + animate: true, + forceHeightChange: true + }); + } + + /** + * Go to the last post in the discussion. + */ + goToLast() { + this.stream.goToLast(); + this.updateScrubberValues({ + animate: true, + forceHeightChange: true + }); + } + onresize() { + // Adjust the height of the scrollbar so that it fills the height of + // the sidebar and doesn't overlap the footer. + const scrubber = this.$(); + const scrollbar = this.$('.Scrubber-scrollbar'); + scrollbar.css('max-height', $(window).height() - scrubber.offset().top + $(window).scrollTop() - parseInt($('#app').css('padding-bottom'), 10) - (scrubber.outerHeight() - scrollbar.outerHeight())); + } + onmousedown(e) { + e.redraw = false; + this.mouseStart = e.clientY || e.originalEvent.touches[0].clientY; + this.indexStart = this.stream.index; + this.dragging = true; + $('body').css('cursor', 'move'); + this.$().toggleClass('dragging', this.dragging); + } + onmousemove(e) { + if (!this.dragging) return; + + // Work out how much the mouse has moved by - first in pixels, then + // convert it to a percentage of the scrollbar's height, and then + // finally convert it into an index. Add this delta index onto + // the index at which the drag was started, and then scroll there. + const deltaPixels = (e.clientY || e.originalEvent.touches[0].clientY) - this.mouseStart; + const deltaPercent = deltaPixels / this.$('.Scrubber-scrollbar').outerHeight() * 100; + const deltaIndex = deltaPercent / this.percentPerPost().index || 0; + const newIndex = Math.min(this.indexStart + deltaIndex, this.stream.count() - 1); + this.stream.index = Math.max(0, newIndex); + this.updateScrubberValues(); + } + onmouseup() { + this.$().toggleClass('dragging', this.dragging); + if (!this.dragging) return; + this.mouseStart = 0; + this.indexStart = 0; + this.dragging = false; + $('body').css('cursor', ''); + this.$().removeClass('open'); + + // If the index we've landed on is in a gap, then tell the stream- + // content that we want to load those posts. + const intIndex = Math.floor(this.stream.index); + this.stream.goToIndex(intIndex); + } + onclick(e) { + // Calculate the index which we want to jump to based on the click position. + + // 1. Get the offset of the click from the top of the scrollbar, as a + // percentage of the scrollbar's height. + const $scrollbar = this.$('.Scrubber-scrollbar'); + const offsetPixels = (e.pageY || e.originalEvent.touches[0].pageY) - $scrollbar.offset().top + $('body').scrollTop(); + let offsetPercent = offsetPixels / $scrollbar.outerHeight() * 100; + + // 2. We want the handle of the scrollbar to end up centered on the click + // position. Thus, we calculate the height of the handle in percent and + // use that to find a new offset percentage. + offsetPercent = offsetPercent - parseFloat($scrollbar.find('.Scrubber-handle')[0].style.height) / 2; + + // 3. Now we can convert the percentage into an index, and tell the stream- + // content component to jump to that index. + let offsetIndex = offsetPercent / this.percentPerPost().index; + offsetIndex = Math.max(0, Math.min(this.stream.count() - 1, offsetIndex)); + this.stream.goToIndex(Math.floor(offsetIndex)); + this.updateScrubberValues({ + animate: true, + forceHeightChange: true + }); + this.$().removeClass('open'); + } + + /** + * Get the percentage of the height of the scrubber that should be allocated + * to each post. + * + * @return {{ index: number, visible: number }} + * @property {Number} index The percent per post for posts on either side of + * the visible part of the scrubber. + * @property {Number} visible The percent per post for the visible part of the + * scrubber. + */ + percentPerPost() { + const count = this.stream.count() || 1; + const visible = this.stream.visible || 1; + + // To stop the handle of the scrollbar from getting too small when there + // are many posts, we define a minimum percentage height for the handle + // calculated from a 50 pixel limit. From this, we can calculate the + // minimum percentage per visible post. If this is greater than the actual + // percentage per post, then we need to adjust the 'before' percentage to + // account for it. + const minPercentVisible = 50 / this.$('.Scrubber-scrollbar').outerHeight() * 100; + const percentPerVisiblePost = Math.max(100 / count, minPercentVisible / visible); + const percentPerPost = count === visible ? 0 : (100 - percentPerVisiblePost * visible) / (count - visible); + return { + index: percentPerPost, + visible: percentPerVisiblePost + }; + } +} +flarum.reg.add('core', 'forum/components/PostStreamScrubber', PostStreamScrubber); + +/***/ }) + +}]); //# sourceMappingURL=PostStreamScrubber.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/PostStreamScrubber.js.map b/framework/core/js/dist/forum/components/PostStreamScrubber.js.map index d3ff109d811..709fc307bae 100644 --- a/framework/core/js/dist/forum/components/PostStreamScrubber.js.map +++ b/framework/core/js/dist/forum/components/PostStreamScrubber.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/PostStreamScrubber.js","mappings":"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","sources":["webpack://@flarum/core/./src/forum/components/PostStreamScrubber.js"],"sourcesContent":["import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport formatNumber from '../../common/utils/formatNumber';\nimport ScrollListener from '../../common/utils/ScrollListener';\nimport Icon from '../../common/components/Icon';\n\n/**\n * The `PostStreamScrubber` component displays a scrubber which can be used to\n * navigate/scrub through a post stream.\n *\n * ### Attrs\n *\n * - `stream`\n * - `className`\n */\nexport default class PostStreamScrubber extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.stream = this.attrs.stream;\n this.handlers = {};\n this.scrollListener = new ScrollListener(this.updateScrubberValues.bind(this, {\n fromScroll: true,\n forceHeightChange: true\n }));\n }\n view() {\n const count = this.stream.count();\n\n // Index is left blank for performance reasons, it is filled in in updateScubberValues\n const viewing = app.translator.trans('core.forum.post_scrubber.viewing_text', {\n count,\n index: m(\"span\", {\n className: \"Scrubber-index\"\n }),\n formattedCount: m(\"span\", {\n className: \"Scrubber-count\"\n }, formatNumber(count))\n });\n const unreadCount = this.stream.discussion.unreadCount();\n const unreadPercent = count ? Math.min(count - this.stream.index, unreadCount) / count : 0;\n function styleUnread(vnode) {\n const $element = $(vnode.dom);\n const newStyle = {\n top: 100 - unreadPercent * 100 + '%',\n height: unreadPercent * 100 + '%',\n opacity: unreadPercent ? 1 : 0\n };\n if (vnode.state.oldStyle) {\n $element.stop(true).css(vnode.state.oldStyle).animate(newStyle);\n } else {\n $element.css(newStyle);\n }\n vnode.state.oldStyle = newStyle;\n }\n const classNames = ['PostStreamScrubber', 'Dropdown'];\n if (this.attrs.className) classNames.push(this.attrs.className);\n return m(\"div\", {\n className: classNames.join(' ')\n }, m(\"button\", {\n className: \"Button Dropdown-toggle\",\n \"data-toggle\": \"dropdown\"\n }, viewing, \" \", m(Icon, {\n name: 'fas fa-sort'\n })), m(\"div\", {\n className: \"Dropdown-menu dropdown-menu\"\n }, m(\"div\", {\n className: \"Scrubber\"\n }, m(\"a\", {\n className: \"Scrubber-first\",\n onclick: this.goToFirst.bind(this)\n }, m(Icon, {\n name: 'fas fa-angle-double-up'\n }), \" \", app.translator.trans('core.forum.post_scrubber.original_post_link')), m(\"div\", {\n className: \"Scrubber-scrollbar\"\n }, m(\"div\", {\n className: \"Scrubber-before\"\n }), m(\"div\", {\n className: \"Scrubber-handle\"\n }, m(\"div\", {\n className: \"Scrubber-bar\"\n }), m(\"div\", {\n className: \"Scrubber-info\"\n }, m(\"strong\", null, viewing), m(\"span\", {\n className: \"Scrubber-description\"\n }))), m(\"div\", {\n className: \"Scrubber-after\"\n }), m(\"div\", {\n className: \"Scrubber-unread\",\n oncreate: styleUnread,\n onupdate: styleUnread\n }, app.translator.trans('core.forum.post_scrubber.unread_text', {\n count: unreadCount\n }))), m(\"a\", {\n className: \"Scrubber-last\",\n onclick: this.goToLast.bind(this)\n }, m(Icon, {\n name: 'fas fa-angle-double-down'\n }), \" \", app.translator.trans('core.forum.post_scrubber.now_link')))));\n }\n onupdate(vnode) {\n super.onupdate(vnode);\n if (this.stream.forceUpdateScrubber) {\n this.stream.forceUpdateScrubber = false;\n this.stream.loadPromise.then(() => this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n }));\n }\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n\n // Whenever the window is resized, adjust the height of the scrollbar\n // so that it fills the height of the sidebar.\n $(window).on('resize', this.handlers.onresize = this.onresize.bind(this)).resize();\n\n // When any part of the whole scrollbar is clicked, we want to jump to\n // that position.\n this.$('.Scrubber-scrollbar').bind('click', this.onclick.bind(this))\n\n // Now we want to make the scrollbar handle draggable. Let's start by\n // preventing default browser events from messing things up.\n .bind('dragstart mousedown touchstart', e => e.preventDefault());\n\n // When the mouse is pressed on the scrollbar handle, we capture some\n // information about its current position. We will store this\n // information in an object and pass it on to the document's\n // mousemove/mouseup events later.\n this.dragging = false;\n this.mouseStart = 0;\n this.indexStart = 0;\n this.$('.Scrubber-handle').bind('mousedown touchstart', this.onmousedown.bind(this))\n\n // Exempt the scrollbar handle from the 'jump to' click event.\n .click(e => e.stopPropagation());\n\n // When the mouse moves and when it is released, we pass the\n // information that we captured when the mouse was first pressed onto\n // some event handlers. These handlers will move the scrollbar/stream-\n // content as appropriate.\n $(document).on('mousemove touchmove', this.handlers.onmousemove = this.onmousemove.bind(this)).on('mouseup touchend', this.handlers.onmouseup = this.onmouseup.bind(this));\n setTimeout(() => this.scrollListener.start());\n this.stream.loadPromise.then(() => this.updateScrubberValues({\n animate: false,\n forceHeightChange: true\n }));\n }\n onremove(vnode) {\n super.onremove(vnode);\n this.scrollListener.stop();\n $(window).off('resize', this.handlers.onresize);\n $(document).off('mousemove touchmove', this.handlers.onmousemove).off('mouseup touchend', this.handlers.onmouseup);\n }\n\n /**\n * Update the scrollbar's position to reflect the current values of the\n * index/visible properties.\n *\n * @param {Partial<{fromScroll: boolean, forceHeightChange: boolean, animate: boolean}>} options\n */\n updateScrubberValues(options) {\n if (options === void 0) {\n options = {};\n }\n const index = this.stream.index;\n const count = this.stream.count();\n const visible = this.stream.visible || 1;\n const percentPerPost = this.percentPerPost();\n const $scrubber = this.$();\n $scrubber.find('.Scrubber-index').text(formatNumber(this.stream.sanitizeIndex(Math.max(1, index))));\n $scrubber.find('.Scrubber-description').text(this.stream.description);\n $scrubber.toggleClass('disabled', this.stream.disabled());\n const heights = {};\n heights.before = Math.max(0, percentPerPost.index * Math.min(index - 1, count - visible));\n heights.handle = Math.min(100 - heights.before, percentPerPost.visible * visible);\n heights.after = 100 - heights.before - heights.handle;\n\n // If the stream is paused, don't change height on scroll, as the viewport is being scrolled by the JS\n // If a height change animation is already in progress, don't adjust height unless overriden\n if (options.fromScroll && this.stream.paused || this.adjustingHeight && !options.forceHeightChange) return;\n const func = options.animate ? 'animate' : 'css';\n this.adjustingHeight = true;\n const animationPromises = [];\n for (const part in heights) {\n const $part = $scrubber.find(\".Scrubber-\".concat(part));\n animationPromises.push($part.stop(true, true)[func]({\n height: heights[part] + '%'\n }, 'fast').promise());\n\n // jQuery likes to put overflow:hidden, but because the scrollbar handle\n // has a negative margin-left, we need to override.\n if (func === 'animate') $part.css('overflow', 'visible');\n }\n Promise.all(animationPromises).then(() => this.adjustingHeight = false);\n }\n\n /**\n * Go to the first post in the discussion.\n */\n goToFirst() {\n this.stream.goToFirst();\n this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n });\n }\n\n /**\n * Go to the last post in the discussion.\n */\n goToLast() {\n this.stream.goToLast();\n this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n });\n }\n onresize() {\n // Adjust the height of the scrollbar so that it fills the height of\n // the sidebar and doesn't overlap the footer.\n const scrubber = this.$();\n const scrollbar = this.$('.Scrubber-scrollbar');\n scrollbar.css('max-height', $(window).height() - scrubber.offset().top + $(window).scrollTop() - parseInt($('#app').css('padding-bottom'), 10) - (scrubber.outerHeight() - scrollbar.outerHeight()));\n }\n onmousedown(e) {\n e.redraw = false;\n this.mouseStart = e.clientY || e.originalEvent.touches[0].clientY;\n this.indexStart = this.stream.index;\n this.dragging = true;\n $('body').css('cursor', 'move');\n this.$().toggleClass('dragging', this.dragging);\n }\n onmousemove(e) {\n if (!this.dragging) return;\n\n // Work out how much the mouse has moved by - first in pixels, then\n // convert it to a percentage of the scrollbar's height, and then\n // finally convert it into an index. Add this delta index onto\n // the index at which the drag was started, and then scroll there.\n const deltaPixels = (e.clientY || e.originalEvent.touches[0].clientY) - this.mouseStart;\n const deltaPercent = deltaPixels / this.$('.Scrubber-scrollbar').outerHeight() * 100;\n const deltaIndex = deltaPercent / this.percentPerPost().index || 0;\n const newIndex = Math.min(this.indexStart + deltaIndex, this.stream.count() - 1);\n this.stream.index = Math.max(0, newIndex);\n this.updateScrubberValues();\n }\n onmouseup() {\n this.$().toggleClass('dragging', this.dragging);\n if (!this.dragging) return;\n this.mouseStart = 0;\n this.indexStart = 0;\n this.dragging = false;\n $('body').css('cursor', '');\n this.$().removeClass('open');\n\n // If the index we've landed on is in a gap, then tell the stream-\n // content that we want to load those posts.\n const intIndex = Math.floor(this.stream.index);\n this.stream.goToIndex(intIndex);\n }\n onclick(e) {\n // Calculate the index which we want to jump to based on the click position.\n\n // 1. Get the offset of the click from the top of the scrollbar, as a\n // percentage of the scrollbar's height.\n const $scrollbar = this.$('.Scrubber-scrollbar');\n const offsetPixels = (e.pageY || e.originalEvent.touches[0].pageY) - $scrollbar.offset().top + $('body').scrollTop();\n let offsetPercent = offsetPixels / $scrollbar.outerHeight() * 100;\n\n // 2. We want the handle of the scrollbar to end up centered on the click\n // position. Thus, we calculate the height of the handle in percent and\n // use that to find a new offset percentage.\n offsetPercent = offsetPercent - parseFloat($scrollbar.find('.Scrubber-handle')[0].style.height) / 2;\n\n // 3. Now we can convert the percentage into an index, and tell the stream-\n // content component to jump to that index.\n let offsetIndex = offsetPercent / this.percentPerPost().index;\n offsetIndex = Math.max(0, Math.min(this.stream.count() - 1, offsetIndex));\n this.stream.goToIndex(Math.floor(offsetIndex));\n this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n });\n this.$().removeClass('open');\n }\n\n /**\n * Get the percentage of the height of the scrubber that should be allocated\n * to each post.\n *\n * @return {{ index: number, visible: number }}\n * @property {Number} index The percent per post for posts on either side of\n * the visible part of the scrubber.\n * @property {Number} visible The percent per post for the visible part of the\n * scrubber.\n */\n percentPerPost() {\n const count = this.stream.count() || 1;\n const visible = this.stream.visible || 1;\n\n // To stop the handle of the scrollbar from getting too small when there\n // are many posts, we define a minimum percentage height for the handle\n // calculated from a 50 pixel limit. From this, we can calculate the\n // minimum percentage per visible post. If this is greater than the actual\n // percentage per post, then we need to adjust the 'before' percentage to\n // account for it.\n const minPercentVisible = 50 / this.$('.Scrubber-scrollbar').outerHeight() * 100;\n const percentPerVisiblePost = Math.max(100 / count, minPercentVisible / visible);\n const percentPerPost = count === visible ? 0 : (100 - percentPerVisiblePost * visible) / (count - visible);\n return {\n index: percentPerPost,\n visible: percentPerVisiblePost\n };\n }\n}\nflarum.reg.add('core', 'forum/components/PostStreamScrubber', PostStreamScrubber);"],"names":["PostStreamScrubber","oninit","vnode","super","this","stream","attrs","handlers","scrollListener","updateScrubberValues","bind","fromScroll","forceHeightChange","view","count","viewing","index","m","className","formattedCount","unreadCount","discussion","unreadPercent","Math","min","styleUnread","$element","$","dom","newStyle","top","height","opacity","state","oldStyle","stop","css","animate","classNames","push","join","name","onclick","goToFirst","oncreate","onupdate","goToLast","forceUpdateScrubber","loadPromise","then","window","on","onresize","resize","e","preventDefault","dragging","mouseStart","indexStart","onmousedown","click","stopPropagation","document","onmousemove","onmouseup","setTimeout","start","onremove","off","options","visible","percentPerPost","$scrubber","find","text","sanitizeIndex","max","description","toggleClass","disabled","heights","before","handle","after","paused","adjustingHeight","func","animationPromises","part","$part","concat","promise","Promise","all","scrubber","scrollbar","offset","scrollTop","parseInt","outerHeight","redraw","clientY","originalEvent","touches","deltaIndex","newIndex","removeClass","intIndex","floor","goToIndex","$scrollbar","offsetPercent","pageY","parseFloat","style","offsetIndex","minPercentVisible","percentPerVisiblePost","flarum","reg","add"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/PostStreamScrubber.js","mappings":";;;;;;;;;;;;;;;;;;AAAkC;AACa;AACY;AACI;AACf;;AAEhD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,iCAAiC,yDAAS;AACzD;AACA;AACA;AACA;AACA,8BAA8B,oEAAc;AAC5C;AACA;AACA,KAAK;AACL;AACA;AACA;;AAEA;AACA,oBAAoB,mEAAoB;AACxC;AACA;AACA;AACA,OAAO;AACP;AACA;AACA,OAAO,EAAE,sEAAY;AACrB,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK,kBAAkB,+DAAI;AAC3B;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,KAAK,QAAQ,mEAAoB;AACjC;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA,KAAK;AACL;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,KAAK,QAAQ,mEAAoB;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa,SAAS,kEAAkE,GAAG;AAC3F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,2CAA2C,sEAAY;AACvD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;;AAEP;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA,gBAAgB;AAChB,gBAAgB,QAAQ;AACxB;AACA,gBAAgB,QAAQ;AACxB;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/PostStreamScrubber.js"],"sourcesContent":["import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport formatNumber from '../../common/utils/formatNumber';\nimport ScrollListener from '../../common/utils/ScrollListener';\nimport Icon from '../../common/components/Icon';\n\n/**\n * The `PostStreamScrubber` component displays a scrubber which can be used to\n * navigate/scrub through a post stream.\n *\n * ### Attrs\n *\n * - `stream`\n * - `className`\n */\nexport default class PostStreamScrubber extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.stream = this.attrs.stream;\n this.handlers = {};\n this.scrollListener = new ScrollListener(this.updateScrubberValues.bind(this, {\n fromScroll: true,\n forceHeightChange: true\n }));\n }\n view() {\n const count = this.stream.count();\n\n // Index is left blank for performance reasons, it is filled in in updateScubberValues\n const viewing = app.translator.trans('core.forum.post_scrubber.viewing_text', {\n count,\n index: m(\"span\", {\n className: \"Scrubber-index\"\n }),\n formattedCount: m(\"span\", {\n className: \"Scrubber-count\"\n }, formatNumber(count))\n });\n const unreadCount = this.stream.discussion.unreadCount();\n const unreadPercent = count ? Math.min(count - this.stream.index, unreadCount) / count : 0;\n function styleUnread(vnode) {\n const $element = $(vnode.dom);\n const newStyle = {\n top: 100 - unreadPercent * 100 + '%',\n height: unreadPercent * 100 + '%',\n opacity: unreadPercent ? 1 : 0\n };\n if (vnode.state.oldStyle) {\n $element.stop(true).css(vnode.state.oldStyle).animate(newStyle);\n } else {\n $element.css(newStyle);\n }\n vnode.state.oldStyle = newStyle;\n }\n const classNames = ['PostStreamScrubber', 'Dropdown'];\n if (this.attrs.className) classNames.push(this.attrs.className);\n return m(\"div\", {\n className: classNames.join(' ')\n }, m(\"button\", {\n className: \"Button Dropdown-toggle\",\n \"data-toggle\": \"dropdown\"\n }, viewing, \" \", m(Icon, {\n name: 'fas fa-sort'\n })), m(\"div\", {\n className: \"Dropdown-menu dropdown-menu\"\n }, m(\"div\", {\n className: \"Scrubber\"\n }, m(\"a\", {\n className: \"Scrubber-first\",\n onclick: this.goToFirst.bind(this)\n }, m(Icon, {\n name: 'fas fa-angle-double-up'\n }), \" \", app.translator.trans('core.forum.post_scrubber.original_post_link')), m(\"div\", {\n className: \"Scrubber-scrollbar\"\n }, m(\"div\", {\n className: \"Scrubber-before\"\n }), m(\"div\", {\n className: \"Scrubber-handle\"\n }, m(\"div\", {\n className: \"Scrubber-bar\"\n }), m(\"div\", {\n className: \"Scrubber-info\"\n }, m(\"strong\", null, viewing), m(\"span\", {\n className: \"Scrubber-description\"\n }))), m(\"div\", {\n className: \"Scrubber-after\"\n }), m(\"div\", {\n className: \"Scrubber-unread\",\n oncreate: styleUnread,\n onupdate: styleUnread\n }, app.translator.trans('core.forum.post_scrubber.unread_text', {\n count: unreadCount\n }))), m(\"a\", {\n className: \"Scrubber-last\",\n onclick: this.goToLast.bind(this)\n }, m(Icon, {\n name: 'fas fa-angle-double-down'\n }), \" \", app.translator.trans('core.forum.post_scrubber.now_link')))));\n }\n onupdate(vnode) {\n super.onupdate(vnode);\n if (this.stream.forceUpdateScrubber) {\n this.stream.forceUpdateScrubber = false;\n this.stream.loadPromise.then(() => this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n }));\n }\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n\n // Whenever the window is resized, adjust the height of the scrollbar\n // so that it fills the height of the sidebar.\n $(window).on('resize', this.handlers.onresize = this.onresize.bind(this)).resize();\n\n // When any part of the whole scrollbar is clicked, we want to jump to\n // that position.\n this.$('.Scrubber-scrollbar').bind('click', this.onclick.bind(this))\n\n // Now we want to make the scrollbar handle draggable. Let's start by\n // preventing default browser events from messing things up.\n .bind('dragstart mousedown touchstart', e => e.preventDefault());\n\n // When the mouse is pressed on the scrollbar handle, we capture some\n // information about its current position. We will store this\n // information in an object and pass it on to the document's\n // mousemove/mouseup events later.\n this.dragging = false;\n this.mouseStart = 0;\n this.indexStart = 0;\n this.$('.Scrubber-handle').bind('mousedown touchstart', this.onmousedown.bind(this))\n\n // Exempt the scrollbar handle from the 'jump to' click event.\n .click(e => e.stopPropagation());\n\n // When the mouse moves and when it is released, we pass the\n // information that we captured when the mouse was first pressed onto\n // some event handlers. These handlers will move the scrollbar/stream-\n // content as appropriate.\n $(document).on('mousemove touchmove', this.handlers.onmousemove = this.onmousemove.bind(this)).on('mouseup touchend', this.handlers.onmouseup = this.onmouseup.bind(this));\n setTimeout(() => this.scrollListener.start());\n this.stream.loadPromise.then(() => this.updateScrubberValues({\n animate: false,\n forceHeightChange: true\n }));\n }\n onremove(vnode) {\n super.onremove(vnode);\n this.scrollListener.stop();\n $(window).off('resize', this.handlers.onresize);\n $(document).off('mousemove touchmove', this.handlers.onmousemove).off('mouseup touchend', this.handlers.onmouseup);\n }\n\n /**\n * Update the scrollbar's position to reflect the current values of the\n * index/visible properties.\n *\n * @param {Partial<{fromScroll: boolean, forceHeightChange: boolean, animate: boolean}>} options\n */\n updateScrubberValues(options) {\n if (options === void 0) {\n options = {};\n }\n const index = this.stream.index;\n const count = this.stream.count();\n const visible = this.stream.visible || 1;\n const percentPerPost = this.percentPerPost();\n const $scrubber = this.$();\n $scrubber.find('.Scrubber-index').text(formatNumber(this.stream.sanitizeIndex(Math.max(1, index))));\n $scrubber.find('.Scrubber-description').text(this.stream.description);\n $scrubber.toggleClass('disabled', this.stream.disabled());\n const heights = {};\n heights.before = Math.max(0, percentPerPost.index * Math.min(index - 1, count - visible));\n heights.handle = Math.min(100 - heights.before, percentPerPost.visible * visible);\n heights.after = 100 - heights.before - heights.handle;\n\n // If the stream is paused, don't change height on scroll, as the viewport is being scrolled by the JS\n // If a height change animation is already in progress, don't adjust height unless overriden\n if (options.fromScroll && this.stream.paused || this.adjustingHeight && !options.forceHeightChange) return;\n const func = options.animate ? 'animate' : 'css';\n this.adjustingHeight = true;\n const animationPromises = [];\n for (const part in heights) {\n const $part = $scrubber.find(\".Scrubber-\".concat(part));\n animationPromises.push($part.stop(true, true)[func]({\n height: heights[part] + '%'\n }, 'fast').promise());\n\n // jQuery likes to put overflow:hidden, but because the scrollbar handle\n // has a negative margin-left, we need to override.\n if (func === 'animate') $part.css('overflow', 'visible');\n }\n Promise.all(animationPromises).then(() => this.adjustingHeight = false);\n }\n\n /**\n * Go to the first post in the discussion.\n */\n goToFirst() {\n this.stream.goToFirst();\n this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n });\n }\n\n /**\n * Go to the last post in the discussion.\n */\n goToLast() {\n this.stream.goToLast();\n this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n });\n }\n onresize() {\n // Adjust the height of the scrollbar so that it fills the height of\n // the sidebar and doesn't overlap the footer.\n const scrubber = this.$();\n const scrollbar = this.$('.Scrubber-scrollbar');\n scrollbar.css('max-height', $(window).height() - scrubber.offset().top + $(window).scrollTop() - parseInt($('#app').css('padding-bottom'), 10) - (scrubber.outerHeight() - scrollbar.outerHeight()));\n }\n onmousedown(e) {\n e.redraw = false;\n this.mouseStart = e.clientY || e.originalEvent.touches[0].clientY;\n this.indexStart = this.stream.index;\n this.dragging = true;\n $('body').css('cursor', 'move');\n this.$().toggleClass('dragging', this.dragging);\n }\n onmousemove(e) {\n if (!this.dragging) return;\n\n // Work out how much the mouse has moved by - first in pixels, then\n // convert it to a percentage of the scrollbar's height, and then\n // finally convert it into an index. Add this delta index onto\n // the index at which the drag was started, and then scroll there.\n const deltaPixels = (e.clientY || e.originalEvent.touches[0].clientY) - this.mouseStart;\n const deltaPercent = deltaPixels / this.$('.Scrubber-scrollbar').outerHeight() * 100;\n const deltaIndex = deltaPercent / this.percentPerPost().index || 0;\n const newIndex = Math.min(this.indexStart + deltaIndex, this.stream.count() - 1);\n this.stream.index = Math.max(0, newIndex);\n this.updateScrubberValues();\n }\n onmouseup() {\n this.$().toggleClass('dragging', this.dragging);\n if (!this.dragging) return;\n this.mouseStart = 0;\n this.indexStart = 0;\n this.dragging = false;\n $('body').css('cursor', '');\n this.$().removeClass('open');\n\n // If the index we've landed on is in a gap, then tell the stream-\n // content that we want to load those posts.\n const intIndex = Math.floor(this.stream.index);\n this.stream.goToIndex(intIndex);\n }\n onclick(e) {\n // Calculate the index which we want to jump to based on the click position.\n\n // 1. Get the offset of the click from the top of the scrollbar, as a\n // percentage of the scrollbar's height.\n const $scrollbar = this.$('.Scrubber-scrollbar');\n const offsetPixels = (e.pageY || e.originalEvent.touches[0].pageY) - $scrollbar.offset().top + $('body').scrollTop();\n let offsetPercent = offsetPixels / $scrollbar.outerHeight() * 100;\n\n // 2. We want the handle of the scrollbar to end up centered on the click\n // position. Thus, we calculate the height of the handle in percent and\n // use that to find a new offset percentage.\n offsetPercent = offsetPercent - parseFloat($scrollbar.find('.Scrubber-handle')[0].style.height) / 2;\n\n // 3. Now we can convert the percentage into an index, and tell the stream-\n // content component to jump to that index.\n let offsetIndex = offsetPercent / this.percentPerPost().index;\n offsetIndex = Math.max(0, Math.min(this.stream.count() - 1, offsetIndex));\n this.stream.goToIndex(Math.floor(offsetIndex));\n this.updateScrubberValues({\n animate: true,\n forceHeightChange: true\n });\n this.$().removeClass('open');\n }\n\n /**\n * Get the percentage of the height of the scrubber that should be allocated\n * to each post.\n *\n * @return {{ index: number, visible: number }}\n * @property {Number} index The percent per post for posts on either side of\n * the visible part of the scrubber.\n * @property {Number} visible The percent per post for the visible part of the\n * scrubber.\n */\n percentPerPost() {\n const count = this.stream.count() || 1;\n const visible = this.stream.visible || 1;\n\n // To stop the handle of the scrollbar from getting too small when there\n // are many posts, we define a minimum percentage height for the handle\n // calculated from a 50 pixel limit. From this, we can calculate the\n // minimum percentage per visible post. If this is greater than the actual\n // percentage per post, then we need to adjust the 'before' percentage to\n // account for it.\n const minPercentVisible = 50 / this.$('.Scrubber-scrollbar').outerHeight() * 100;\n const percentPerVisiblePost = Math.max(100 / count, minPercentVisible / visible);\n const percentPerPost = count === visible ? 0 : (100 - percentPerVisiblePost * visible) / (count - visible);\n return {\n index: percentPerPost,\n visible: percentPerVisiblePost\n };\n }\n}\nflarum.reg.add('core', 'forum/components/PostStreamScrubber', PostStreamScrubber);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/ReplyComposer.js b/framework/core/js/dist/forum/components/ReplyComposer.js index bfb0b4d0260..c8ac3e00c99 100644 --- a/framework/core/js/dist/forum/components/ReplyComposer.js +++ b/framework/core/js/dist/forum/components/ReplyComposer.js @@ -1,2 +1,296 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[630],{2140:(s,e,t)=>{t.d(e,{Z:()=>h});var o=t(2190),r=t(5226);class i extends o.Z{handler(){return this.attrs.when()||void 0}oncreate(s){super.oncreate(s),this.boundHandler=this.handler.bind(this),$(window).on("beforeunload",this.boundHandler)}onremove(s){super.onremove(s),$(window).off("beforeunload",this.boundHandler)}view(s){return m("[",null,s.children)}}flarum.reg.add("core","common/components/ConfirmDocumentUnload",i);var n=t(4944),a=t(1268),c=t(4041),l=t(3344),d=t(7323);class h extends o.Z{oninit(s){super.oninit(s),this.composer=this.attrs.composer,this.loading=!1,this.attrs.confirmExit&&this.composer.preventClosingWhen((()=>this.hasChanges()),this.attrs.confirmExit),this.composer.fields.content(this.attrs.originalContent||"")}view(){var s;return m(i,{when:this.hasChanges.bind(this)},m("div",{className:(0,l.Z)("ComposerBody",this.attrs.className)},m(d.Z,{user:this.attrs.user,className:"ComposerBody-avatar"}),m("div",{className:"ComposerBody-content"},m("ul",{className:"ComposerBody-header"},(0,a.Z)(this.headerItems().toArray())),m("div",{className:"ComposerBody-editor"},m(n.Z,{submitLabel:this.attrs.submitLabel,placeholder:this.attrs.placeholder,disabled:this.loading||this.attrs.disabled,composer:this.composer,preview:null==(s=this.jumpToPreview)?void 0:s.bind(this),onchange:this.composer.fields.content,onsubmit:this.onsubmit.bind(this),value:this.composer.fields.content()}))),m(r.Z,{display:"unset",containerClassName:(0,l.Z)("ComposerBody-loading",this.loading&&"active"),size:"large"})))}hasChanges(){const s=this.composer.fields.content();return s&&s!==this.attrs.originalContent}headerItems(){return new c.Z}onsubmit(){}loaded(){this.loading=!1,m.redraw()}}flarum.reg.add("core","forum/components/ComposerBody",h)},2925:(s,e,t)=>{t.r(e),t.d(e,{default:()=>d});var o=t(6789),r=t(2140),i=t(8312),n=t(6597),a=t(1552),c=t(9133);function l(s){o.Z.composer.isFullScreen()&&(o.Z.composer.minimize(),s.stopPropagation())}class d extends r.Z{static initAttrs(s){super.initAttrs(s),s.placeholder=s.placeholder||(0,a.Z)(o.Z.translator.trans("core.forum.composer_reply.body_placeholder")),s.submitLabel=s.submitLabel||o.Z.translator.trans("core.forum.composer_reply.submit_button"),s.confirmExit=s.confirmExit||(0,a.Z)(o.Z.translator.trans("core.forum.composer_reply.discard_confirmation"))}headerItems(){const s=super.headerItems(),e=this.attrs.discussion;return s.add("title",m("h3",null,m(c.Z,{name:"fas fa-reply"})," ",m(n.Z,{href:o.Z.route.discussion(e),onclick:l},e.title()))),s}jumpToPreview(s){l(s),m.route.set(o.Z.route.discussion(this.attrs.discussion,"reply"))}data(){return{content:this.composer.fields.content(),relationships:{discussion:this.attrs.discussion}}}onsubmit(){const s=this.attrs.discussion;this.loading=!0,m.redraw();const e=this.data();o.Z.store.createRecord("posts").save(e).then((e=>{if(o.Z.viewingDiscussion(s)){const s=o.Z.current.get("stream");s.update().then((()=>s.goToNumber(e.number())))}else{let s;const t=m(i.Z,{className:"Button Button--link",onclick:()=>{m.route.set(o.Z.route.post(e)),o.Z.alerts.dismiss(s)}},o.Z.translator.trans("core.forum.composer_reply.view_button"));s=o.Z.alerts.show({type:"success",controls:[t]},o.Z.translator.trans("core.forum.composer_reply.posted_message"))}this.composer.hide()}),this.loaded.bind(this))}}flarum.reg.add("core","forum/components/ReplyComposer",d)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/ReplyComposer"],{ + +/***/ "./src/common/components/ConfirmDocumentUnload.js": +/*!********************************************************!*\ + !*** ./src/common/components/ConfirmDocumentUnload.js ***! + \********************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ConfirmDocumentUnload) +/* harmony export */ }); +/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ "./src/common/Component.ts"); + + +/** + * The `ConfirmDocumentUnload` component can be used to register a global + * event handler that prevents closing the browser window/tab based on the + * return value of a given callback prop. + * + * ### Attrs + * + * - `when` - a callback returning true when the browser should prompt for + * confirmation before closing the window/tab + */ +class ConfirmDocumentUnload extends _Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + handler() { + return this.attrs.when() || undefined; + } + oncreate(vnode) { + super.oncreate(vnode); + this.boundHandler = this.handler.bind(this); + $(window).on('beforeunload', this.boundHandler); + } + onremove(vnode) { + super.onremove(vnode); + $(window).off('beforeunload', this.boundHandler); + } + view(vnode) { + return m('[', null, vnode.children); + } +} +flarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload); + +/***/ }), + +/***/ "./src/forum/components/ComposerBody.js": +/*!**********************************************!*\ + !*** ./src/forum/components/ComposerBody.js ***! + \**********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ComposerBody) +/* harmony export */ }); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx"); +/* harmony import */ var _common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/ConfirmDocumentUnload */ "./src/common/components/ConfirmDocumentUnload.js"); +/* harmony import */ var _common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/TextEditor */ "./src/common/components/TextEditor.js"); +/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts"); +/* harmony import */ var _common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Avatar */ "./src/common/components/Avatar.tsx"); + + + + + + + + + +/** + * The `ComposerBody` component handles the body, or the content, of the + * composer. Subclasses should implement the `onsubmit` method and override + * `headerTimes`. + * + * ### Attrs + * + * - `composer` + * - `originalContent` + * - `submitLabel` + * - `placeholder` + * - `user` + * - `confirmExit` + * - `disabled` + * + * @abstract + */ +class ComposerBody extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + oninit(vnode) { + super.oninit(vnode); + this.composer = this.attrs.composer; + + /** + * Whether or not the component is loading. + * + * @type {Boolean} + */ + this.loading = false; + + // Let the composer state know to ask for confirmation under certain + // circumstances, if the body supports / requires it and has a corresponding + // confirmation question to ask. + if (this.attrs.confirmExit) { + this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit); + } + this.composer.fields.content(this.attrs.originalContent || ''); + } + view() { + var _this$jumpToPreview; + return m(_common_components_ConfirmDocumentUnload__WEBPACK_IMPORTED_MODULE_2__["default"], { + when: this.hasChanges.bind(this) + }, m("div", { + className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody', this.attrs.className) + }, m(_common_components_Avatar__WEBPACK_IMPORTED_MODULE_7__["default"], { + user: this.attrs.user, + className: "ComposerBody-avatar" + }), m("div", { + className: "ComposerBody-content" + }, m("ul", { + className: "ComposerBody-header" + }, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_4__["default"])(this.headerItems().toArray())), m("div", { + className: "ComposerBody-editor" + }, m(_common_components_TextEditor__WEBPACK_IMPORTED_MODULE_3__["default"], { + submitLabel: this.attrs.submitLabel, + placeholder: this.attrs.placeholder, + disabled: this.loading || this.attrs.disabled, + composer: this.composer, + preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this), + onchange: this.composer.fields.content, + onsubmit: this.onsubmit.bind(this), + value: this.composer.fields.content() + }))), m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_1__["default"], { + display: "unset", + containerClassName: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_6__["default"])('ComposerBody-loading', this.loading && 'active'), + size: "large" + }))); + } + + /** + * Check if there is any unsaved data. + * + * @return {boolean} + */ + hasChanges() { + const content = this.composer.fields.content(); + return content && content !== this.attrs.originalContent; + } + + /** + * Build an item list for the composer's header. + * + * @return {ItemList} + */ + headerItems() { + return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"](); + } + + /** + * Handle the submit event of the text editor. + * + * @abstract + */ + onsubmit() {} + + /** + * Stop loading. + */ + loaded() { + this.loading = false; + m.redraw(); + } +} +flarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody); + +/***/ }), + +/***/ "./src/forum/components/ReplyComposer.js": +/*!***********************************************!*\ + !*** ./src/forum/components/ReplyComposer.js ***! + \***********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ReplyComposer) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _ComposerBody__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ComposerBody */ "./src/forum/components/ComposerBody.js"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_components_Link__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Link */ "./src/common/components/Link.js"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_components_Icon__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/components/Icon */ "./src/common/components/Icon.tsx"); + + + + + + +function minimizeComposerIfFullScreen(e) { + if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.isFullScreen()) { + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].composer.minimize(); + e.stopPropagation(); + } +} + +/** + * The `ReplyComposer` component displays the composer content for replying to a + * discussion. + * + * ### Attrs + * + * - All of the attrs of ComposerBody + * - `discussion` + */ +class ReplyComposer extends _ComposerBody__WEBPACK_IMPORTED_MODULE_1__["default"] { + static initAttrs(attrs) { + super.initAttrs(attrs); + attrs.placeholder = attrs.placeholder || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.body_placeholder')); + attrs.submitLabel = attrs.submitLabel || _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.submit_button'); + attrs.confirmExit = attrs.confirmExit || (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_4__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.discard_confirmation')); + } + headerItems() { + const items = super.headerItems(); + const discussion = this.attrs.discussion; + items.add('title', m("h3", null, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_5__["default"], { + name: 'fas fa-reply' + }), ' ', m(_common_components_Link__WEBPACK_IMPORTED_MODULE_3__["default"], { + href: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(discussion), + onclick: minimizeComposerIfFullScreen + }, discussion.title()))); + return items; + } + + /** + * Jump to the preview when triggered by the text editor. + */ + jumpToPreview(e) { + minimizeComposerIfFullScreen(e); + m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.discussion(this.attrs.discussion, 'reply')); + } + + /** + * Get the data to submit to the server when the reply is saved. + * + * @return {Record} + */ + data() { + return { + content: this.composer.fields.content(), + relationships: { + discussion: this.attrs.discussion + } + }; + } + onsubmit() { + const discussion = this.attrs.discussion; + this.loading = true; + m.redraw(); + const data = this.data(); + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].store.createRecord('posts').save(data).then(post => { + // If we're currently viewing the discussion which this reply was made + // in, then we can update the post stream and scroll to the post. + if (_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].viewingDiscussion(discussion)) { + const stream = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].current.get('stream'); + stream.update().then(() => stream.goToNumber(post.number())); + } else { + // Otherwise, we'll create an alert message to inform the user that + // their reply has been posted, containing a button which will + // transition to their new post when clicked. + let alert; + const viewButton = m(_common_components_Button__WEBPACK_IMPORTED_MODULE_2__["default"], { + className: "Button Button--link", + onclick: () => { + m.route.set(_forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].route.post(post)); + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.dismiss(alert); + } + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.view_button')); + alert = _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].alerts.show({ + type: 'success', + controls: [viewButton] + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.composer_reply.posted_message')); + } + this.composer.hide(); + }, this.loaded.bind(this)); + } +} +flarum.reg.add('core', 'forum/components/ReplyComposer', ReplyComposer); + +/***/ }) + +}]); //# sourceMappingURL=ReplyComposer.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/ReplyComposer.js.map b/framework/core/js/dist/forum/components/ReplyComposer.js.map index c949fa64219..3c9b3938392 100644 --- a/framework/core/js/dist/forum/components/ReplyComposer.js.map +++ b/framework/core/js/dist/forum/components/ReplyComposer.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/ReplyComposer.js","mappings":"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","sources":["webpack://@flarum/core/./src/common/components/ConfirmDocumentUnload.js","webpack://@flarum/core/./src/forum/components/ComposerBody.js","webpack://@flarum/core/./src/forum/components/ReplyComposer.js"],"sourcesContent":["import Component from '../Component';\n\n/**\n * The `ConfirmDocumentUnload` component can be used to register a global\n * event handler that prevents closing the browser window/tab based on the\n * return value of a given callback prop.\n *\n * ### Attrs\n *\n * - `when` - a callback returning true when the browser should prompt for\n * confirmation before closing the window/tab\n */\nexport default class ConfirmDocumentUnload extends Component {\n handler() {\n return this.attrs.when() || undefined;\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.boundHandler = this.handler.bind(this);\n $(window).on('beforeunload', this.boundHandler);\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('beforeunload', this.boundHandler);\n }\n view(vnode) {\n return m('[', null, vnode.children);\n }\n}\nflarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);","import Component from '../../common/Component';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';\nimport TextEditor from '../../common/components/TextEditor';\nimport listItems from '../../common/helpers/listItems';\nimport ItemList from '../../common/utils/ItemList';\nimport classList from '../../common/utils/classList';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ComposerBody` component handles the body, or the content, of the\n * composer. Subclasses should implement the `onsubmit` method and override\n * `headerTimes`.\n *\n * ### Attrs\n *\n * - `composer`\n * - `originalContent`\n * - `submitLabel`\n * - `placeholder`\n * - `user`\n * - `confirmExit`\n * - `disabled`\n *\n * @abstract\n */\nexport default class ComposerBody extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.composer = this.attrs.composer;\n\n /**\n * Whether or not the component is loading.\n *\n * @type {Boolean}\n */\n this.loading = false;\n\n // Let the composer state know to ask for confirmation under certain\n // circumstances, if the body supports / requires it and has a corresponding\n // confirmation question to ask.\n if (this.attrs.confirmExit) {\n this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);\n }\n this.composer.fields.content(this.attrs.originalContent || '');\n }\n view() {\n var _this$jumpToPreview;\n return m(ConfirmDocumentUnload, {\n when: this.hasChanges.bind(this)\n }, m(\"div\", {\n className: classList('ComposerBody', this.attrs.className)\n }, m(Avatar, {\n user: this.attrs.user,\n className: \"ComposerBody-avatar\"\n }), m(\"div\", {\n className: \"ComposerBody-content\"\n }, m(\"ul\", {\n className: \"ComposerBody-header\"\n }, listItems(this.headerItems().toArray())), m(\"div\", {\n className: \"ComposerBody-editor\"\n }, m(TextEditor, {\n submitLabel: this.attrs.submitLabel,\n placeholder: this.attrs.placeholder,\n disabled: this.loading || this.attrs.disabled,\n composer: this.composer,\n preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),\n onchange: this.composer.fields.content,\n onsubmit: this.onsubmit.bind(this),\n value: this.composer.fields.content()\n }))), m(LoadingIndicator, {\n display: \"unset\",\n containerClassName: classList('ComposerBody-loading', this.loading && 'active'),\n size: \"large\"\n })));\n }\n\n /**\n * Check if there is any unsaved data.\n *\n * @return {boolean}\n */\n hasChanges() {\n const content = this.composer.fields.content();\n return content && content !== this.attrs.originalContent;\n }\n\n /**\n * Build an item list for the composer's header.\n *\n * @return {ItemList}\n */\n headerItems() {\n return new ItemList();\n }\n\n /**\n * Handle the submit event of the text editor.\n *\n * @abstract\n */\n onsubmit() {}\n\n /**\n * Stop loading.\n */\n loaded() {\n this.loading = false;\n m.redraw();\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);","import app from '../../forum/app';\nimport ComposerBody from './ComposerBody';\nimport Button from '../../common/components/Button';\nimport Link from '../../common/components/Link';\nimport extractText from '../../common/utils/extractText';\nimport Icon from '../../common/components/Icon';\nfunction minimizeComposerIfFullScreen(e) {\n if (app.composer.isFullScreen()) {\n app.composer.minimize();\n e.stopPropagation();\n }\n}\n\n/**\n * The `ReplyComposer` component displays the composer content for replying to a\n * discussion.\n *\n * ### Attrs\n *\n * - All of the attrs of ComposerBody\n * - `discussion`\n */\nexport default class ReplyComposer extends ComposerBody {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.placeholder = attrs.placeholder || extractText(app.translator.trans('core.forum.composer_reply.body_placeholder'));\n attrs.submitLabel = attrs.submitLabel || app.translator.trans('core.forum.composer_reply.submit_button');\n attrs.confirmExit = attrs.confirmExit || extractText(app.translator.trans('core.forum.composer_reply.discard_confirmation'));\n }\n headerItems() {\n const items = super.headerItems();\n const discussion = this.attrs.discussion;\n items.add('title', m(\"h3\", null, m(Icon, {\n name: 'fas fa-reply'\n }), ' ', m(Link, {\n href: app.route.discussion(discussion),\n onclick: minimizeComposerIfFullScreen\n }, discussion.title())));\n return items;\n }\n\n /**\n * Jump to the preview when triggered by the text editor.\n */\n jumpToPreview(e) {\n minimizeComposerIfFullScreen(e);\n m.route.set(app.route.discussion(this.attrs.discussion, 'reply'));\n }\n\n /**\n * Get the data to submit to the server when the reply is saved.\n *\n * @return {Record}\n */\n data() {\n return {\n content: this.composer.fields.content(),\n relationships: {\n discussion: this.attrs.discussion\n }\n };\n }\n onsubmit() {\n const discussion = this.attrs.discussion;\n this.loading = true;\n m.redraw();\n const data = this.data();\n app.store.createRecord('posts').save(data).then(post => {\n // If we're currently viewing the discussion which this reply was made\n // in, then we can update the post stream and scroll to the post.\n if (app.viewingDiscussion(discussion)) {\n const stream = app.current.get('stream');\n stream.update().then(() => stream.goToNumber(post.number()));\n } else {\n // Otherwise, we'll create an alert message to inform the user that\n // their reply has been posted, containing a button which will\n // transition to their new post when clicked.\n let alert;\n const viewButton = m(Button, {\n className: \"Button Button--link\",\n onclick: () => {\n m.route.set(app.route.post(post));\n app.alerts.dismiss(alert);\n }\n }, app.translator.trans('core.forum.composer_reply.view_button'));\n alert = app.alerts.show({\n type: 'success',\n controls: [viewButton]\n }, app.translator.trans('core.forum.composer_reply.posted_message'));\n }\n this.composer.hide();\n }, this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/ReplyComposer', ReplyComposer);"],"names":["ConfirmDocumentUnload","Component","handler","this","attrs","when","undefined","oncreate","vnode","super","boundHandler","bind","$","window","on","onremove","off","view","m","children","flarum","reg","add","ComposerBody","oninit","composer","loading","confirmExit","preventClosingWhen","hasChanges","fields","content","originalContent","_this$jumpToPreview","className","classList","Avatar","user","listItems","headerItems","toArray","TextEditor","submitLabel","placeholder","disabled","preview","jumpToPreview","onchange","onsubmit","value","LoadingIndicator","display","containerClassName","size","ItemList","loaded","redraw","minimizeComposerIfFullScreen","e","stopPropagation","ReplyComposer","static","initAttrs","items","discussion","name","href","onclick","title","route","set","data","relationships","save","then","post","stream","update","goToNumber","number","alert","viewButton","type","controls","hide"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/ReplyComposer.js","mappings":";;;;;;;;;;;;;;AAAqC;;AAErC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,oCAAoC,kDAAS;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;AC7B+C;AACyB;AACU;AACtB;AACL;AACJ;AACE;AACD;;AAEpD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,2BAA2B,yDAAS;AACnD;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,gFAAqB;AAClC;AACA,KAAK;AACL,iBAAiB,mEAAS;AAC1B,KAAK,IAAI,iEAAM;AACf;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK,EAAE,qEAAS;AAChB;AACA,KAAK,IAAI,qEAAU;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,OAAO,2EAAgB;AAC5B;AACA,0BAA0B,mEAAS;AACnC;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA,eAAe,8DAAQ;AACvB;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;AC/GkC;AACQ;AACU;AACJ;AACS;AACT;AAChD;AACA,MAAM,wEAAyB;AAC/B,IAAI,oEAAqB;AACzB;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,4BAA4B,qDAAY;AACvD;AACA;AACA,6CAA6C,qEAAW,CAAC,mEAAoB;AAC7E,6CAA6C,mEAAoB;AACjE,6CAA6C,qEAAW,CAAC,mEAAoB;AAC7E;AACA;AACA;AACA;AACA,uCAAuC,+DAAI;AAC3C;AACA,KAAK,UAAU,+DAAI;AACnB,YAAY,mEAAoB;AAChC;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,gBAAgB,mEAAoB;AACpC;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,qEAAsB;AAC1B;AACA;AACA,UAAU,oEAAqB;AAC/B,uBAAuB,8DAAe;AACtC;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA,6BAA6B,iEAAM;AACnC;AACA;AACA,wBAAwB,6DAAc;AACtC,YAAY,iEAAkB;AAC9B;AACA,SAAS,EAAE,mEAAoB;AAC/B,gBAAgB,8DAAe;AAC/B;AACA;AACA,SAAS,EAAE,mEAAoB;AAC/B;AACA;AACA,KAAK;AACL;AACA;AACA","sources":["webpack://@flarum/core/./src/common/components/ConfirmDocumentUnload.js","webpack://@flarum/core/./src/forum/components/ComposerBody.js","webpack://@flarum/core/./src/forum/components/ReplyComposer.js"],"sourcesContent":["import Component from '../Component';\n\n/**\n * The `ConfirmDocumentUnload` component can be used to register a global\n * event handler that prevents closing the browser window/tab based on the\n * return value of a given callback prop.\n *\n * ### Attrs\n *\n * - `when` - a callback returning true when the browser should prompt for\n * confirmation before closing the window/tab\n */\nexport default class ConfirmDocumentUnload extends Component {\n handler() {\n return this.attrs.when() || undefined;\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.boundHandler = this.handler.bind(this);\n $(window).on('beforeunload', this.boundHandler);\n }\n onremove(vnode) {\n super.onremove(vnode);\n $(window).off('beforeunload', this.boundHandler);\n }\n view(vnode) {\n return m('[', null, vnode.children);\n }\n}\nflarum.reg.add('core', 'common/components/ConfirmDocumentUnload', ConfirmDocumentUnload);","import Component from '../../common/Component';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';\nimport TextEditor from '../../common/components/TextEditor';\nimport listItems from '../../common/helpers/listItems';\nimport ItemList from '../../common/utils/ItemList';\nimport classList from '../../common/utils/classList';\nimport Avatar from '../../common/components/Avatar';\n\n/**\n * The `ComposerBody` component handles the body, or the content, of the\n * composer. Subclasses should implement the `onsubmit` method and override\n * `headerTimes`.\n *\n * ### Attrs\n *\n * - `composer`\n * - `originalContent`\n * - `submitLabel`\n * - `placeholder`\n * - `user`\n * - `confirmExit`\n * - `disabled`\n *\n * @abstract\n */\nexport default class ComposerBody extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.composer = this.attrs.composer;\n\n /**\n * Whether or not the component is loading.\n *\n * @type {Boolean}\n */\n this.loading = false;\n\n // Let the composer state know to ask for confirmation under certain\n // circumstances, if the body supports / requires it and has a corresponding\n // confirmation question to ask.\n if (this.attrs.confirmExit) {\n this.composer.preventClosingWhen(() => this.hasChanges(), this.attrs.confirmExit);\n }\n this.composer.fields.content(this.attrs.originalContent || '');\n }\n view() {\n var _this$jumpToPreview;\n return m(ConfirmDocumentUnload, {\n when: this.hasChanges.bind(this)\n }, m(\"div\", {\n className: classList('ComposerBody', this.attrs.className)\n }, m(Avatar, {\n user: this.attrs.user,\n className: \"ComposerBody-avatar\"\n }), m(\"div\", {\n className: \"ComposerBody-content\"\n }, m(\"ul\", {\n className: \"ComposerBody-header\"\n }, listItems(this.headerItems().toArray())), m(\"div\", {\n className: \"ComposerBody-editor\"\n }, m(TextEditor, {\n submitLabel: this.attrs.submitLabel,\n placeholder: this.attrs.placeholder,\n disabled: this.loading || this.attrs.disabled,\n composer: this.composer,\n preview: (_this$jumpToPreview = this.jumpToPreview) == null ? void 0 : _this$jumpToPreview.bind(this),\n onchange: this.composer.fields.content,\n onsubmit: this.onsubmit.bind(this),\n value: this.composer.fields.content()\n }))), m(LoadingIndicator, {\n display: \"unset\",\n containerClassName: classList('ComposerBody-loading', this.loading && 'active'),\n size: \"large\"\n })));\n }\n\n /**\n * Check if there is any unsaved data.\n *\n * @return {boolean}\n */\n hasChanges() {\n const content = this.composer.fields.content();\n return content && content !== this.attrs.originalContent;\n }\n\n /**\n * Build an item list for the composer's header.\n *\n * @return {ItemList}\n */\n headerItems() {\n return new ItemList();\n }\n\n /**\n * Handle the submit event of the text editor.\n *\n * @abstract\n */\n onsubmit() {}\n\n /**\n * Stop loading.\n */\n loaded() {\n this.loading = false;\n m.redraw();\n }\n}\nflarum.reg.add('core', 'forum/components/ComposerBody', ComposerBody);","import app from '../../forum/app';\nimport ComposerBody from './ComposerBody';\nimport Button from '../../common/components/Button';\nimport Link from '../../common/components/Link';\nimport extractText from '../../common/utils/extractText';\nimport Icon from '../../common/components/Icon';\nfunction minimizeComposerIfFullScreen(e) {\n if (app.composer.isFullScreen()) {\n app.composer.minimize();\n e.stopPropagation();\n }\n}\n\n/**\n * The `ReplyComposer` component displays the composer content for replying to a\n * discussion.\n *\n * ### Attrs\n *\n * - All of the attrs of ComposerBody\n * - `discussion`\n */\nexport default class ReplyComposer extends ComposerBody {\n static initAttrs(attrs) {\n super.initAttrs(attrs);\n attrs.placeholder = attrs.placeholder || extractText(app.translator.trans('core.forum.composer_reply.body_placeholder'));\n attrs.submitLabel = attrs.submitLabel || app.translator.trans('core.forum.composer_reply.submit_button');\n attrs.confirmExit = attrs.confirmExit || extractText(app.translator.trans('core.forum.composer_reply.discard_confirmation'));\n }\n headerItems() {\n const items = super.headerItems();\n const discussion = this.attrs.discussion;\n items.add('title', m(\"h3\", null, m(Icon, {\n name: 'fas fa-reply'\n }), ' ', m(Link, {\n href: app.route.discussion(discussion),\n onclick: minimizeComposerIfFullScreen\n }, discussion.title())));\n return items;\n }\n\n /**\n * Jump to the preview when triggered by the text editor.\n */\n jumpToPreview(e) {\n minimizeComposerIfFullScreen(e);\n m.route.set(app.route.discussion(this.attrs.discussion, 'reply'));\n }\n\n /**\n * Get the data to submit to the server when the reply is saved.\n *\n * @return {Record}\n */\n data() {\n return {\n content: this.composer.fields.content(),\n relationships: {\n discussion: this.attrs.discussion\n }\n };\n }\n onsubmit() {\n const discussion = this.attrs.discussion;\n this.loading = true;\n m.redraw();\n const data = this.data();\n app.store.createRecord('posts').save(data).then(post => {\n // If we're currently viewing the discussion which this reply was made\n // in, then we can update the post stream and scroll to the post.\n if (app.viewingDiscussion(discussion)) {\n const stream = app.current.get('stream');\n stream.update().then(() => stream.goToNumber(post.number()));\n } else {\n // Otherwise, we'll create an alert message to inform the user that\n // their reply has been posted, containing a button which will\n // transition to their new post when clicked.\n let alert;\n const viewButton = m(Button, {\n className: \"Button Button--link\",\n onclick: () => {\n m.route.set(app.route.post(post));\n app.alerts.dismiss(alert);\n }\n }, app.translator.trans('core.forum.composer_reply.view_button'));\n alert = app.alerts.show({\n type: 'success',\n controls: [viewButton]\n }, app.translator.trans('core.forum.composer_reply.posted_message'));\n }\n this.composer.hide();\n }, this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/ReplyComposer', ReplyComposer);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/SearchModal.js b/framework/core/js/dist/forum/components/SearchModal.js index 14be5cbb142..14e312d408c 100644 --- a/framework/core/js/dist/forum/components/SearchModal.js +++ b/framework/core/js/dist/forum/components/SearchModal.js @@ -1,2 +1,386 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[616],{7502:(e,t,s)=>{s.r(t),s.d(t,{default:()=>f});var a=s(7905),r=s(6789),i=s(899),o=s(8046),c=s(8740),n=s(1552),l=s(9908),h=s(8312),u=s(6458),d=s(3986),p=s(5226),v=s(4041),g=s(4945);class f extends i.Z{constructor(){super(...arguments),(0,a.Z)(this,"searchState",void 0),(0,a.Z)(this,"query",void 0),(0,a.Z)(this,"sources",void 0),(0,a.Z)(this,"activeSource",void 0),(0,a.Z)(this,"loadingSources",[]),(0,a.Z)(this,"index",0),(0,a.Z)(this,"navigator",void 0),(0,a.Z)(this,"searchTimeout",void 0),(0,a.Z)(this,"inputScroll",(0,u.Z)(0)),(0,a.Z)(this,"gambitsAutocomplete",{})}oninit(e){super.oninit(e),this.searchState=this.attrs.searchState,this.sources=this.attrs.sources,this.query=(0,u.Z)(this.searchState.getValue()||"")}title(){return r.Z.translator.trans("core.forum.search.title")}className(){return"SearchModal Modal--flat"}content(){var e,t;this.activeSource||(this.activeSource=(0,u.Z)(this.sources[0])),(e=this.gambitsAutocomplete)[t=this.activeSource().resource]||(e[t]=new g.Z(this.activeSource().resource,(()=>this.inputElement()),this.query,(e=>this.search(e))));const s=(0,n.Z)(r.Z.translator.trans("core.forum.search.placeholder"));return m("div",{className:"Modal-body SearchModal-body"},m("div",{className:"SearchModal-form"},m(l.Z,{key:"search",type:"search",loading:!!this.loadingSources.length,clearable:!0,clearLabel:r.Z.translator.trans("core.forum.header.search_clear_button_accessible_label"),prefixIcon:"fas fa-search","aria-label":s,placeholder:s,value:this.query(),onchange:e=>{var t,s;this.query(e),this.inputScroll(null!=(t=null==(s=this.inputElement()[0])?void 0:s.scrollLeft)?t:0)},inputAttrs:{className:"SearchModal-input"},renderInput:e=>m("[",null,m("input",Object.assign({},e,{onscroll:e=>this.inputScroll(e.target.scrollLeft)})),m("div",{className:"SearchModal-visual-wrapper"},m("div",{className:"SearchModal-visual-input",style:{left:"-"+this.inputScroll()+"px"}},this.gambifyInput())))})),this.tabs())}tabs(){return m("div",{className:"SearchModal-tabs"},m("div",{className:"SearchModal-tabs-nav"},this.tabItems().toArray()),m("div",{className:"SearchModal-tabs-content"},this.activeTabItems().toArray()))}tabItems(){var e;const t=new v.Z;return null==(e=this.sources)||e.map(((e,s)=>t.add(e.resource,m(h.Z,{className:"Button Button--link",active:this.activeSource()===e,onclick:()=>this.switchSource(e)},e.title()),100-s))),t}activeTabItems(){var e;const t=new v.Z,s=this.loadingSources.includes(this.activeSource().resource),a=!!this.query()&&!s,i=this.gambits(),o=this.activeSource().fullPage(this.query()),c=null==(e=this.activeSource())?void 0:e.view(this.query());return a&&o&&t.add("fullPageLink",m("div",{className:"SearchModal-section"},m("hr",{className:"Modal-divider"}),m("ul",{className:"Dropdown-menu SearchModal-fullPage"},o)),80),i.length&&t.add("gambits",m("div",{className:"SearchModal-section"},m("hr",{className:"Modal-divider"}),m("ul",{className:"Dropdown-menu SearchModal-options","aria-live":i.length?"polite":void 0},m("li",{className:"Dropdown-header"},r.Z.translator.trans("core.forum.search.options_heading")),i)),60),t.add("results",m("div",{className:"SearchModal-section"},m("hr",{className:"Modal-divider"}),m("ul",{className:"Dropdown-menu SearchModal-results","aria-live":a?"polite":void 0},m("li",{className:"Dropdown-header"},r.Z.translator.trans("core.forum.search.preview_heading")),!a&&m("li",{className:"Dropdown-message"},m(d.Z,{icon:"fas fa-search"},r.Z.translator.trans("core.forum.search.no_search_text"))),a&&c,a&&!(null!=c&&c.length)&&m("li",{className:"Dropdown-message"},m(d.Z,{icon:"far fa-tired"},r.Z.translator.trans("core.forum.search.no_results_text"))),s&&m("li",{className:"Dropdown-message"},m(p.Z,null)))),40),t}switchSource(e){this.activeSource()!==e&&(this.activeSource(e),this.search(this.query()),this.inputElement().focus(),m.redraw())}gambits(){return this.gambitsAutocomplete[this.activeSource().resource].suggestions(this.query())}gambifyInput(){const e=this.query();let t=e;r.Z.search.gambits.match(this.activeSource().resource,e,((e,s,a,r)=>{t=t.replace(r,"".concat(r,""))}));const s=[];return t.split(/(.*?<\/mark>)/).forEach((e=>{e.startsWith("")?s.push(m("mark",null,e.replace(/<\/?mark>/g,""))):s.push(e)})),s}onupdate(e){var t;super.onupdate(e),this.setIndex(this.getCurrentNumericIndex());const s=this;this.$(".Dropdown-menu").on("mouseenter","> li:not(.Dropdown-header):not(.Dropdown-message)",(function(){s.setIndex(s.selectableItems().index(this))})),null!=(t=this.sources)&&t.length}oncreate(e){var t;if(super.oncreate(e),null==(t=this.sources)||!t.length)return;const s=this.search.bind(this);this.setIndex(this.getCurrentNumericIndex());const a=this.inputElement();this.navigator=new o.Z,this.navigator.onUp((()=>this.setIndex(this.getCurrentNumericIndex()-1,!0))).onDown((()=>this.setIndex(this.getCurrentNumericIndex()+1,!0))).onSelect(this.selectResult.bind(this),!0).onCancel(this.clear.bind(this)).bindTo(a),a.on("input focus",(function(){s(this.value.toLowerCase())}))}onremove(e){this.searchState.setValue(this.query()),super.onremove(e)}search(e){if(!e)return;const t=this.activeSource();this.searchTimeout&&clearTimeout(this.searchTimeout),this.searchTimeout=window.setTimeout((()=>{if(!t.isCached(e)){if(e.length>=c.Z.MIN_SEARCH_LEN){if(!t.search)return;this.loadingSources.push(t.resource),t.search(e,f.LIMIT).then((()=>{this.loadingSources=this.loadingSources.filter((e=>e!==t.resource)),m.redraw()}))}this.searchState.cache(e),m.redraw()}}),250)}selectResult(){this.searchTimeout&&clearTimeout(this.searchTimeout),this.loadingSources=[];const e=this.getItem(this.index);let t=null;if(e.attr("data-id")){const s=e.attr("data-id");t=s&&this.activeSource().gotoItem(s)}else e.find("a").length&&(t=e.find("a").attr("href"));this.query()&&t?m.route.set(t):e.find("button")[0].click()}clear(){this.query("")}selectableItems(){return this.$(".Dropdown-menu > li:not(.Dropdown-header):not(.Dropdown-message)")}getCurrentNumericIndex(){return Math.max(0,this.selectableItems().index(this.getItem(this.index)))}getItem(e){const t=this.selectableItems();let s=t.filter('[data-index="'.concat(e,'"]'));return s.length||(s=t.eq(e)),s}setIndex(e,t){void 0===t&&(t=!1);const s=this.selectableItems(),a=s.parent();let r=e;e<0?r=s.length-1:e>=s.length&&(r=0);const i=s.removeClass("active").eq(r).addClass("active");if(this.index=parseInt(i.attr("data-index"))||r,t&&a){const e=a.scrollTop(),t=a.offset().top,s=t+a.outerHeight(),r=i.offset().top,o=r+i.outerHeight();let c;rs&&(c=e-s+o+parseInt(a.css("padding-bottom"),10)),void 0!==c&&a.stop(!0).animate({scrollTop:c},100)}}inputElement(){return this.$(".SearchModal-input")}}(0,a.Z)(f,"LIMIT",6),flarum.reg.add("core","forum/components/SearchModal",f)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/SearchModal"],{ + +/***/ "./src/forum/components/SearchModal.tsx": +/*!**********************************************!*\ + !*** ./src/forum/components/SearchModal.tsx ***! + \**********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ SearchModal) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _common_utils_KeyboardNavigatable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/KeyboardNavigatable */ "./src/common/utils/KeyboardNavigatable.ts"); +/* harmony import */ var _common_SearchManager__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/SearchManager */ "./src/common/SearchManager.ts"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_components_Input__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/components/Input */ "./src/common/components/Input.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts"); +/* harmony import */ var _common_components_InfoTile__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../../common/components/InfoTile */ "./src/common/components/InfoTile.tsx"); +/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_utils_GambitsAutocomplete__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../../common/utils/GambitsAutocomplete */ "./src/common/utils/GambitsAutocomplete.tsx"); + + + + + + + + + + + + + +class SearchModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "searchState", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "query", void 0); + /** + * An array of SearchSources. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "sources", void 0); + /** + * The key of the currently-active search source. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "activeSource", void 0); + /** + * The sources that are still loading results. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "loadingSources", []); + /** + * The index of the currently-selected
  • in the results list. This can be + * a unique string (to account for the fact that an item's position may jump + * around as new results load), but otherwise it will be numeric (the + * sequential position within the list). + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "index", 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "navigator", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "searchTimeout", void 0); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "inputScroll", (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(0)); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "gambitsAutocomplete", {}); + } + oninit(vnode) { + super.oninit(vnode); + this.searchState = this.attrs.searchState; + this.sources = this.attrs.sources; + this.query = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(this.searchState.getValue() || ''); + } + title() { + return _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.search.title'); + } + className() { + return 'SearchModal Modal--flat'; + } + content() { + var _this$gambitsAutocomp, _this$activeSource$re; + // Initialize the active source. + if (!this.activeSource) this.activeSource = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_8__["default"])(this.sources[0]); + (_this$gambitsAutocomp = this.gambitsAutocomplete)[_this$activeSource$re = this.activeSource().resource] || (_this$gambitsAutocomp[_this$activeSource$re] = new _common_utils_GambitsAutocomplete__WEBPACK_IMPORTED_MODULE_12__["default"](this.activeSource().resource, () => this.inputElement(), this.query, value => this.search(value))); + const searchLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.search.placeholder')); + return m("div", { + className: "Modal-body SearchModal-body" + }, m("div", { + className: "SearchModal-form" + }, m(_common_components_Input__WEBPACK_IMPORTED_MODULE_6__["default"], { + key: "search", + type: "search", + loading: !!this.loadingSources.length, + clearable: true, + clearLabel: _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.header.search_clear_button_accessible_label'), + prefixIcon: "fas fa-search", + "aria-label": searchLabel, + placeholder: searchLabel, + value: this.query(), + onchange: value => { + var _this$inputElement$0$, _this$inputElement$; + this.query(value); + this.inputScroll((_this$inputElement$0$ = (_this$inputElement$ = this.inputElement()[0]) == null ? void 0 : _this$inputElement$.scrollLeft) != null ? _this$inputElement$0$ : 0); + }, + inputAttrs: { + className: 'SearchModal-input' + }, + renderInput: attrs => m('[', null, m("input", Object.assign({}, attrs, { + onscroll: e => this.inputScroll(e.target.scrollLeft) + })), m("div", { + className: "SearchModal-visual-wrapper" + }, m("div", { + className: "SearchModal-visual-input", + style: { + left: '-' + this.inputScroll() + 'px' + } + }, this.gambifyInput()))) + })), this.tabs()); + } + tabs() { + return m("div", { + className: "SearchModal-tabs" + }, m("div", { + className: "SearchModal-tabs-nav" + }, this.tabItems().toArray()), m("div", { + className: "SearchModal-tabs-content" + }, this.activeTabItems().toArray())); + } + tabItems() { + var _this$sources; + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_11__["default"](); + (_this$sources = this.sources) == null ? void 0 : _this$sources.map((source, index) => items.add(source.resource, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_7__["default"], { + className: "Button Button--link", + active: this.activeSource() === source, + onclick: () => this.switchSource(source) + }, source.title()), 100 - index)); + return items; + } + activeTabItems() { + var _this$activeSource; + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_11__["default"](); + const loading = this.loadingSources.includes(this.activeSource().resource); + const shouldShowResults = !!this.query() && !loading; + const gambits = this.gambits(); + const fullPageLink = this.activeSource().fullPage(this.query()); + const results = (_this$activeSource = this.activeSource()) == null ? void 0 : _this$activeSource.view(this.query()); + if (shouldShowResults && fullPageLink) { + items.add('fullPageLink', m("div", { + className: "SearchModal-section" + }, m("hr", { + className: "Modal-divider" + }), m("ul", { + className: "Dropdown-menu SearchModal-fullPage" + }, fullPageLink)), 80); + } + if (!!gambits.length) { + items.add('gambits', m("div", { + className: "SearchModal-section" + }, m("hr", { + className: "Modal-divider" + }), m("ul", { + className: "Dropdown-menu SearchModal-options", + "aria-live": gambits.length ? 'polite' : undefined + }, m("li", { + className: "Dropdown-header" + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.search.options_heading')), gambits)), 60); + } + items.add('results', m("div", { + className: "SearchModal-section" + }, m("hr", { + className: "Modal-divider" + }), m("ul", { + className: "Dropdown-menu SearchModal-results", + "aria-live": shouldShowResults ? 'polite' : undefined + }, m("li", { + className: "Dropdown-header" + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.search.preview_heading')), !shouldShowResults && m("li", { + className: "Dropdown-message" + }, m(_common_components_InfoTile__WEBPACK_IMPORTED_MODULE_9__["default"], { + icon: "fas fa-search" + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.search.no_search_text'))), shouldShowResults && results, shouldShowResults && !(results != null && results.length) && m("li", { + className: "Dropdown-message" + }, m(_common_components_InfoTile__WEBPACK_IMPORTED_MODULE_9__["default"], { + icon: "far fa-tired" + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.search.no_results_text'))), loading && m("li", { + className: "Dropdown-message" + }, m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_10__["default"], null)))), 40); + return items; + } + switchSource(source) { + if (this.activeSource() !== source) { + this.activeSource(source); + this.search(this.query()); + this.inputElement().focus(); + m.redraw(); + } + } + gambits() { + return this.gambitsAutocomplete[this.activeSource().resource].suggestions(this.query()); + } + + /** + * Transforms a simple search text to wrap valid gambits in a mark tag. + * @example `lorem ipsum is:unread dolor` => `lorem ipsum is:unread dolor` + */ + gambifyInput() { + const query = this.query(); + let marked = query; + _app__WEBPACK_IMPORTED_MODULE_1__["default"].search.gambits.match(this.activeSource().resource, query, (gambit, matches, negate, bit) => { + marked = marked.replace(bit, "".concat(bit, "")); + }); + const jsx = []; + marked.split(/(.*?<\/mark>)/).forEach(chunk => { + if (chunk.startsWith('')) { + jsx.push(m("mark", null, chunk.replace(/<\/?mark>/g, ''))); + } else { + jsx.push(chunk); + } + }); + return jsx; + } + onupdate(vnode) { + var _this$sources2; + super.onupdate(vnode); + + // Highlight the item that is currently selected. + this.setIndex(this.getCurrentNumericIndex()); + const component = this; + this.$('.Dropdown-menu') + // Whenever the mouse is hovered over a search result, highlight it. + .on('mouseenter', '> li:not(.Dropdown-header):not(.Dropdown-message)', function () { + component.setIndex(component.selectableItems().index(this)); + }); + + // If there are no sources, the search view is not shown. + if (!((_this$sources2 = this.sources) != null && _this$sources2.length)) return; + } + oncreate(vnode) { + var _this$sources3; + super.oncreate(vnode); + + // If there are no sources, we shouldn't initialize logic for + // search elements, as they will not be shown. + if (!((_this$sources3 = this.sources) != null && _this$sources3.length)) return; + const search = this.search.bind(this); + + // Highlight the item that is currently selected. + this.setIndex(this.getCurrentNumericIndex()); + const $input = this.inputElement(); + this.navigator = new _common_utils_KeyboardNavigatable__WEBPACK_IMPORTED_MODULE_3__["default"](); + this.navigator.onUp(() => this.setIndex(this.getCurrentNumericIndex() - 1, true)).onDown(() => this.setIndex(this.getCurrentNumericIndex() + 1, true)).onSelect(this.selectResult.bind(this), true).onCancel(this.clear.bind(this)).bindTo($input); + + // Handle input key events on the search input, triggering results to load. + $input.on('input focus', function () { + search(this.value.toLowerCase()); + }); + } + onremove(vnode) { + this.searchState.setValue(this.query()); + super.onremove(vnode); + } + search(query) { + if (!query) return; + const source = this.activeSource(); + if (this.searchTimeout) clearTimeout(this.searchTimeout); + this.searchTimeout = window.setTimeout(() => { + if (source.isCached(query)) return; + if (query.length >= _common_SearchManager__WEBPACK_IMPORTED_MODULE_4__["default"].MIN_SEARCH_LEN) { + if (!source.search) return; + this.loadingSources.push(source.resource); + source.search(query, SearchModal.LIMIT).then(() => { + this.loadingSources = this.loadingSources.filter(resource => resource !== source.resource); + m.redraw(); + }); + } + this.searchState.cache(query); + m.redraw(); + }, 250); + } + + /** + * Navigate to the currently selected search result and close the list. + */ + selectResult() { + if (this.searchTimeout) clearTimeout(this.searchTimeout); + this.loadingSources = []; + const item = this.getItem(this.index); + const isResult = !!item.attr('data-id'); + let selectedUrl = null; + if (isResult) { + const id = item.attr('data-id'); + selectedUrl = id && this.activeSource().gotoItem(id); + } else if (item.find('a').length) { + selectedUrl = item.find('a').attr('href'); + } + const query = this.query(); + if (query && selectedUrl) { + m.route.set(selectedUrl); + } else { + item.find('button')[0].click(); + } + } + + /** + * Clear the search + */ + clear() { + this.query(''); + } + + /** + * Get all of the search result items that are selectable. + */ + selectableItems() { + return this.$('.Dropdown-menu > li:not(.Dropdown-header):not(.Dropdown-message)'); + } + + /** + * Get the position of the currently selected search result item. + * Returns zero if not found. + */ + getCurrentNumericIndex() { + return Math.max(0, this.selectableItems().index(this.getItem(this.index))); + } + + /** + * Get the
  • in the search results with the given index (numeric or named). + */ + getItem(index) { + const $items = this.selectableItems(); + let $item = $items.filter("[data-index=\"".concat(index, "\"]")); + if (!$item.length) { + $item = $items.eq(index); + } + return $item; + } + + /** + * Set the currently-selected search result item to the one with the given + * index. + */ + setIndex(index, scrollToItem) { + if (scrollToItem === void 0) { + scrollToItem = false; + } + const $items = this.selectableItems(); + const $dropdown = $items.parent(); + let fixedIndex = index; + if (index < 0) { + fixedIndex = $items.length - 1; + } else if (index >= $items.length) { + fixedIndex = 0; + } + const $item = $items.removeClass('active').eq(fixedIndex).addClass('active'); + this.index = parseInt($item.attr('data-index')) || fixedIndex; + if (scrollToItem && $dropdown) { + const dropdownScroll = $dropdown.scrollTop(); + const dropdownTop = $dropdown.offset().top; + const dropdownBottom = dropdownTop + $dropdown.outerHeight(); + const itemTop = $item.offset().top; + const itemBottom = itemTop + $item.outerHeight(); + let scrollTop; + if (itemTop < dropdownTop) { + scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10); + } else if (itemBottom > dropdownBottom) { + scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10); + } + if (typeof scrollTop !== 'undefined') { + $dropdown.stop(true).animate({ + scrollTop + }, 100); + } + } + } + inputElement() { + return this.$('.SearchModal-input'); + } +} +(0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(SearchModal, "LIMIT", 6); +flarum.reg.add('core', 'forum/components/SearchModal', SearchModal); + +/***/ }) + +}]); //# sourceMappingURL=SearchModal.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/SearchModal.js.map b/framework/core/js/dist/forum/components/SearchModal.js.map index de45288439f..00ec1af1d18 100644 --- a/framework/core/js/dist/forum/components/SearchModal.js.map +++ b/framework/core/js/dist/forum/components/SearchModal.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/SearchModal.js","mappings":"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","sources":["webpack://@flarum/core/./src/forum/components/SearchModal.tsx"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../app';\nimport FormModal from '../../common/components/FormModal';\nimport KeyboardNavigatable from '../../common/utils/KeyboardNavigatable';\nimport SearchManager from '../../common/SearchManager';\nimport extractText from '../../common/utils/extractText';\nimport Input from '../../common/components/Input';\nimport Button from '../../common/components/Button';\nimport Stream from '../../common/utils/Stream';\nimport InfoTile from '../../common/components/InfoTile';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ItemList from '../../common/utils/ItemList';\nimport GambitsAutocomplete from '../../common/utils/GambitsAutocomplete';\nexport default class SearchModal extends FormModal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"searchState\", void 0);\n _defineProperty(this, \"query\", void 0);\n /**\n * An array of SearchSources.\n */\n _defineProperty(this, \"sources\", void 0);\n /**\n * The key of the currently-active search source.\n */\n _defineProperty(this, \"activeSource\", void 0);\n /**\n * The sources that are still loading results.\n */\n _defineProperty(this, \"loadingSources\", []);\n /**\n * The index of the currently-selected
  • in the results list. This can be\n * a unique string (to account for the fact that an item's position may jump\n * around as new results load), but otherwise it will be numeric (the\n * sequential position within the list).\n */\n _defineProperty(this, \"index\", 0);\n _defineProperty(this, \"navigator\", void 0);\n _defineProperty(this, \"searchTimeout\", void 0);\n _defineProperty(this, \"inputScroll\", Stream(0));\n _defineProperty(this, \"gambitsAutocomplete\", {});\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.searchState = this.attrs.searchState;\n this.sources = this.attrs.sources;\n this.query = Stream(this.searchState.getValue() || '');\n }\n title() {\n return app.translator.trans('core.forum.search.title');\n }\n className() {\n return 'SearchModal Modal--flat';\n }\n content() {\n var _this$gambitsAutocomp, _this$activeSource$re;\n // Initialize the active source.\n if (!this.activeSource) this.activeSource = Stream(this.sources[0]);\n (_this$gambitsAutocomp = this.gambitsAutocomplete)[_this$activeSource$re = this.activeSource().resource] || (_this$gambitsAutocomp[_this$activeSource$re] = new GambitsAutocomplete(this.activeSource().resource, () => this.inputElement(), this.query, value => this.search(value)));\n const searchLabel = extractText(app.translator.trans('core.forum.search.placeholder'));\n return m(\"div\", {\n className: \"Modal-body SearchModal-body\"\n }, m(\"div\", {\n className: \"SearchModal-form\"\n }, m(Input, {\n key: \"search\",\n type: \"search\",\n loading: !!this.loadingSources.length,\n clearable: true,\n clearLabel: app.translator.trans('core.forum.header.search_clear_button_accessible_label'),\n prefixIcon: \"fas fa-search\",\n \"aria-label\": searchLabel,\n placeholder: searchLabel,\n value: this.query(),\n onchange: value => {\n var _this$inputElement$0$, _this$inputElement$;\n this.query(value);\n this.inputScroll((_this$inputElement$0$ = (_this$inputElement$ = this.inputElement()[0]) == null ? void 0 : _this$inputElement$.scrollLeft) != null ? _this$inputElement$0$ : 0);\n },\n inputAttrs: {\n className: 'SearchModal-input'\n },\n renderInput: attrs => m('[', null, m(\"input\", Object.assign({}, attrs, {\n onscroll: e => this.inputScroll(e.target.scrollLeft)\n })), m(\"div\", {\n className: \"SearchModal-visual-wrapper\"\n }, m(\"div\", {\n className: \"SearchModal-visual-input\",\n style: {\n left: '-' + this.inputScroll() + 'px'\n }\n }, this.gambifyInput())))\n })), this.tabs());\n }\n tabs() {\n return m(\"div\", {\n className: \"SearchModal-tabs\"\n }, m(\"div\", {\n className: \"SearchModal-tabs-nav\"\n }, this.tabItems().toArray()), m(\"div\", {\n className: \"SearchModal-tabs-content\"\n }, this.activeTabItems().toArray()));\n }\n tabItems() {\n var _this$sources;\n const items = new ItemList();\n (_this$sources = this.sources) == null ? void 0 : _this$sources.map((source, index) => items.add(source.resource, m(Button, {\n className: \"Button Button--link\",\n active: this.activeSource() === source,\n onclick: () => this.switchSource(source)\n }, source.title()), 100 - index));\n return items;\n }\n activeTabItems() {\n var _this$activeSource;\n const items = new ItemList();\n const loading = this.loadingSources.includes(this.activeSource().resource);\n const shouldShowResults = !!this.query() && !loading;\n const gambits = this.gambits();\n const fullPageLink = this.activeSource().fullPage(this.query());\n const results = (_this$activeSource = this.activeSource()) == null ? void 0 : _this$activeSource.view(this.query());\n if (shouldShowResults && fullPageLink) {\n items.add('fullPageLink', m(\"div\", {\n className: \"SearchModal-section\"\n }, m(\"hr\", {\n className: \"Modal-divider\"\n }), m(\"ul\", {\n className: \"Dropdown-menu SearchModal-fullPage\"\n }, fullPageLink)), 80);\n }\n if (!!gambits.length) {\n items.add('gambits', m(\"div\", {\n className: \"SearchModal-section\"\n }, m(\"hr\", {\n className: \"Modal-divider\"\n }), m(\"ul\", {\n className: \"Dropdown-menu SearchModal-options\",\n \"aria-live\": gambits.length ? 'polite' : undefined\n }, m(\"li\", {\n className: \"Dropdown-header\"\n }, app.translator.trans('core.forum.search.options_heading')), gambits)), 60);\n }\n items.add('results', m(\"div\", {\n className: \"SearchModal-section\"\n }, m(\"hr\", {\n className: \"Modal-divider\"\n }), m(\"ul\", {\n className: \"Dropdown-menu SearchModal-results\",\n \"aria-live\": shouldShowResults ? 'polite' : undefined\n }, m(\"li\", {\n className: \"Dropdown-header\"\n }, app.translator.trans('core.forum.search.preview_heading')), !shouldShowResults && m(\"li\", {\n className: \"Dropdown-message\"\n }, m(InfoTile, {\n icon: \"fas fa-search\"\n }, app.translator.trans('core.forum.search.no_search_text'))), shouldShowResults && results, shouldShowResults && !(results != null && results.length) && m(\"li\", {\n className: \"Dropdown-message\"\n }, m(InfoTile, {\n icon: \"far fa-tired\"\n }, app.translator.trans('core.forum.search.no_results_text'))), loading && m(\"li\", {\n className: \"Dropdown-message\"\n }, m(LoadingIndicator, null)))), 40);\n return items;\n }\n switchSource(source) {\n if (this.activeSource() !== source) {\n this.activeSource(source);\n this.search(this.query());\n this.inputElement().focus();\n m.redraw();\n }\n }\n gambits() {\n return this.gambitsAutocomplete[this.activeSource().resource].suggestions(this.query());\n }\n\n /**\n * Transforms a simple search text to wrap valid gambits in a mark tag.\n * @example `lorem ipsum is:unread dolor` => `lorem ipsum is:unread dolor`\n */\n gambifyInput() {\n const query = this.query();\n let marked = query;\n app.search.gambits.match(this.activeSource().resource, query, (gambit, matches, negate, bit) => {\n marked = marked.replace(bit, \"\".concat(bit, \"\"));\n });\n const jsx = [];\n marked.split(/(.*?<\\/mark>)/).forEach(chunk => {\n if (chunk.startsWith('')) {\n jsx.push(m(\"mark\", null, chunk.replace(/<\\/?mark>/g, '')));\n } else {\n jsx.push(chunk);\n }\n });\n return jsx;\n }\n onupdate(vnode) {\n var _this$sources2;\n super.onupdate(vnode);\n\n // Highlight the item that is currently selected.\n this.setIndex(this.getCurrentNumericIndex());\n const component = this;\n this.$('.Dropdown-menu')\n // Whenever the mouse is hovered over a search result, highlight it.\n .on('mouseenter', '> li:not(.Dropdown-header):not(.Dropdown-message)', function () {\n component.setIndex(component.selectableItems().index(this));\n });\n\n // If there are no sources, the search view is not shown.\n if (!((_this$sources2 = this.sources) != null && _this$sources2.length)) return;\n }\n oncreate(vnode) {\n var _this$sources3;\n super.oncreate(vnode);\n\n // If there are no sources, we shouldn't initialize logic for\n // search elements, as they will not be shown.\n if (!((_this$sources3 = this.sources) != null && _this$sources3.length)) return;\n const search = this.search.bind(this);\n\n // Highlight the item that is currently selected.\n this.setIndex(this.getCurrentNumericIndex());\n const $input = this.inputElement();\n this.navigator = new KeyboardNavigatable();\n this.navigator.onUp(() => this.setIndex(this.getCurrentNumericIndex() - 1, true)).onDown(() => this.setIndex(this.getCurrentNumericIndex() + 1, true)).onSelect(this.selectResult.bind(this), true).onCancel(this.clear.bind(this)).bindTo($input);\n\n // Handle input key events on the search input, triggering results to load.\n $input.on('input focus', function () {\n search(this.value.toLowerCase());\n });\n }\n onremove(vnode) {\n this.searchState.setValue(this.query());\n super.onremove(vnode);\n }\n search(query) {\n if (!query) return;\n const source = this.activeSource();\n if (this.searchTimeout) clearTimeout(this.searchTimeout);\n this.searchTimeout = window.setTimeout(() => {\n if (source.isCached(query)) return;\n if (query.length >= SearchManager.MIN_SEARCH_LEN) {\n if (!source.search) return;\n this.loadingSources.push(source.resource);\n source.search(query, SearchModal.LIMIT).then(() => {\n this.loadingSources = this.loadingSources.filter(resource => resource !== source.resource);\n m.redraw();\n });\n }\n this.searchState.cache(query);\n m.redraw();\n }, 250);\n }\n\n /**\n * Navigate to the currently selected search result and close the list.\n */\n selectResult() {\n if (this.searchTimeout) clearTimeout(this.searchTimeout);\n this.loadingSources = [];\n const item = this.getItem(this.index);\n const isResult = !!item.attr('data-id');\n let selectedUrl = null;\n if (isResult) {\n const id = item.attr('data-id');\n selectedUrl = id && this.activeSource().gotoItem(id);\n } else if (item.find('a').length) {\n selectedUrl = item.find('a').attr('href');\n }\n const query = this.query();\n if (query && selectedUrl) {\n m.route.set(selectedUrl);\n } else {\n item.find('button')[0].click();\n }\n }\n\n /**\n * Clear the search\n */\n clear() {\n this.query('');\n }\n\n /**\n * Get all of the search result items that are selectable.\n */\n selectableItems() {\n return this.$('.Dropdown-menu > li:not(.Dropdown-header):not(.Dropdown-message)');\n }\n\n /**\n * Get the position of the currently selected search result item.\n * Returns zero if not found.\n */\n getCurrentNumericIndex() {\n return Math.max(0, this.selectableItems().index(this.getItem(this.index)));\n }\n\n /**\n * Get the
  • in the search results with the given index (numeric or named).\n */\n getItem(index) {\n const $items = this.selectableItems();\n let $item = $items.filter(\"[data-index=\\\"\".concat(index, \"\\\"]\"));\n if (!$item.length) {\n $item = $items.eq(index);\n }\n return $item;\n }\n\n /**\n * Set the currently-selected search result item to the one with the given\n * index.\n */\n setIndex(index, scrollToItem) {\n if (scrollToItem === void 0) {\n scrollToItem = false;\n }\n const $items = this.selectableItems();\n const $dropdown = $items.parent();\n let fixedIndex = index;\n if (index < 0) {\n fixedIndex = $items.length - 1;\n } else if (index >= $items.length) {\n fixedIndex = 0;\n }\n const $item = $items.removeClass('active').eq(fixedIndex).addClass('active');\n this.index = parseInt($item.attr('data-index')) || fixedIndex;\n if (scrollToItem && $dropdown) {\n const dropdownScroll = $dropdown.scrollTop();\n const dropdownTop = $dropdown.offset().top;\n const dropdownBottom = dropdownTop + $dropdown.outerHeight();\n const itemTop = $item.offset().top;\n const itemBottom = itemTop + $item.outerHeight();\n let scrollTop;\n if (itemTop < dropdownTop) {\n scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);\n } else if (itemBottom > dropdownBottom) {\n scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);\n }\n if (typeof scrollTop !== 'undefined') {\n $dropdown.stop(true).animate({\n scrollTop\n }, 100);\n }\n }\n }\n inputElement() {\n return this.$('.SearchModal-input');\n }\n}\n_defineProperty(SearchModal, \"LIMIT\", 6);\nflarum.reg.add('core', 'forum/components/SearchModal', SearchModal);"],"names":["SearchModal","constructor","super","arguments","this","oninit","vnode","searchState","attrs","sources","query","getValue","title","className","content","_this$gambitsAutocomp","_this$activeSource$re","activeSource","gambitsAutocomplete","resource","inputElement","value","search","searchLabel","m","key","type","loading","loadingSources","length","clearable","clearLabel","prefixIcon","placeholder","onchange","_this$inputElement$0$","_this$inputElement$","inputScroll","scrollLeft","inputAttrs","renderInput","Object","assign","onscroll","e","target","style","left","gambifyInput","tabs","tabItems","toArray","activeTabItems","_this$sources","items","map","source","index","add","active","onclick","switchSource","_this$activeSource","includes","shouldShowResults","gambits","fullPageLink","fullPage","results","view","undefined","icon","focus","redraw","suggestions","marked","gambit","matches","negate","bit","replace","concat","jsx","split","forEach","chunk","startsWith","push","onupdate","_this$sources2","setIndex","getCurrentNumericIndex","component","$","on","selectableItems","oncreate","_this$sources3","bind","$input","navigator","onUp","onDown","onSelect","selectResult","onCancel","clear","bindTo","toLowerCase","onremove","setValue","searchTimeout","clearTimeout","window","setTimeout","isCached","LIMIT","then","filter","cache","item","getItem","selectedUrl","attr","id","gotoItem","find","route","set","click","Math","max","$items","$item","eq","scrollToItem","$dropdown","parent","fixedIndex","removeClass","addClass","parseInt","dropdownScroll","scrollTop","dropdownTop","offset","top","dropdownBottom","outerHeight","itemTop","itemBottom","css","stop","animate","flarum","reg"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/SearchModal.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAwE;AAC/C;AACiC;AACe;AAClB;AACE;AACP;AACE;AACL;AACS;AACgB;AACrB;AACsB;AAC1D,0BAA0B,oEAAS;AAClD;AACA;AACA,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB,IAAI,qFAAe,sBAAsB,gEAAM;AAC/C,IAAI,qFAAe,gCAAgC;AACnD;AACA;AACA;AACA;AACA;AACA,iBAAiB,gEAAM;AACvB;AACA;AACA,WAAW,6DAAoB;AAC/B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gDAAgD,gEAAM;AACtD,oKAAoK,0EAAmB;AACvL,wBAAwB,qEAAW,CAAC,6DAAoB;AACxD;AACA;AACA,KAAK;AACL;AACA,KAAK,IAAI,gEAAK;AACd;AACA;AACA;AACA;AACA,kBAAkB,6DAAoB;AACtC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA,OAAO;AACP,oEAAoE;AACpE;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA;AACA;AACA;AACA,OAAO;AACP,KAAK;AACL;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA;AACA,sBAAsB,+DAAQ;AAC9B,wHAAwH,iEAAM;AAC9H;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA,sBAAsB,+DAAQ;AAC9B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA;AACA;AACA;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA;AACA,OAAO;AACP;AACA,OAAO,EAAE,6DAAoB;AAC7B;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA,KAAK,IAAI,mEAAQ;AACjB;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA,KAAK,IAAI,mEAAQ;AACjB;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA,KAAK,IAAI,4EAAgB;AACzB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,iEAAwB;AAC5B;AACA,KAAK;AACL;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;;AAEL;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,yBAAyB,yEAAmB;AAC5C;;AAEA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0BAA0B,4EAA4B;AACtD;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,QAAQ;AACR;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qFAAe;AACf","sources":["webpack://@flarum/core/./src/forum/components/SearchModal.tsx"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../app';\nimport FormModal from '../../common/components/FormModal';\nimport KeyboardNavigatable from '../../common/utils/KeyboardNavigatable';\nimport SearchManager from '../../common/SearchManager';\nimport extractText from '../../common/utils/extractText';\nimport Input from '../../common/components/Input';\nimport Button from '../../common/components/Button';\nimport Stream from '../../common/utils/Stream';\nimport InfoTile from '../../common/components/InfoTile';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport ItemList from '../../common/utils/ItemList';\nimport GambitsAutocomplete from '../../common/utils/GambitsAutocomplete';\nexport default class SearchModal extends FormModal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"searchState\", void 0);\n _defineProperty(this, \"query\", void 0);\n /**\n * An array of SearchSources.\n */\n _defineProperty(this, \"sources\", void 0);\n /**\n * The key of the currently-active search source.\n */\n _defineProperty(this, \"activeSource\", void 0);\n /**\n * The sources that are still loading results.\n */\n _defineProperty(this, \"loadingSources\", []);\n /**\n * The index of the currently-selected
  • in the results list. This can be\n * a unique string (to account for the fact that an item's position may jump\n * around as new results load), but otherwise it will be numeric (the\n * sequential position within the list).\n */\n _defineProperty(this, \"index\", 0);\n _defineProperty(this, \"navigator\", void 0);\n _defineProperty(this, \"searchTimeout\", void 0);\n _defineProperty(this, \"inputScroll\", Stream(0));\n _defineProperty(this, \"gambitsAutocomplete\", {});\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.searchState = this.attrs.searchState;\n this.sources = this.attrs.sources;\n this.query = Stream(this.searchState.getValue() || '');\n }\n title() {\n return app.translator.trans('core.forum.search.title');\n }\n className() {\n return 'SearchModal Modal--flat';\n }\n content() {\n var _this$gambitsAutocomp, _this$activeSource$re;\n // Initialize the active source.\n if (!this.activeSource) this.activeSource = Stream(this.sources[0]);\n (_this$gambitsAutocomp = this.gambitsAutocomplete)[_this$activeSource$re = this.activeSource().resource] || (_this$gambitsAutocomp[_this$activeSource$re] = new GambitsAutocomplete(this.activeSource().resource, () => this.inputElement(), this.query, value => this.search(value)));\n const searchLabel = extractText(app.translator.trans('core.forum.search.placeholder'));\n return m(\"div\", {\n className: \"Modal-body SearchModal-body\"\n }, m(\"div\", {\n className: \"SearchModal-form\"\n }, m(Input, {\n key: \"search\",\n type: \"search\",\n loading: !!this.loadingSources.length,\n clearable: true,\n clearLabel: app.translator.trans('core.forum.header.search_clear_button_accessible_label'),\n prefixIcon: \"fas fa-search\",\n \"aria-label\": searchLabel,\n placeholder: searchLabel,\n value: this.query(),\n onchange: value => {\n var _this$inputElement$0$, _this$inputElement$;\n this.query(value);\n this.inputScroll((_this$inputElement$0$ = (_this$inputElement$ = this.inputElement()[0]) == null ? void 0 : _this$inputElement$.scrollLeft) != null ? _this$inputElement$0$ : 0);\n },\n inputAttrs: {\n className: 'SearchModal-input'\n },\n renderInput: attrs => m('[', null, m(\"input\", Object.assign({}, attrs, {\n onscroll: e => this.inputScroll(e.target.scrollLeft)\n })), m(\"div\", {\n className: \"SearchModal-visual-wrapper\"\n }, m(\"div\", {\n className: \"SearchModal-visual-input\",\n style: {\n left: '-' + this.inputScroll() + 'px'\n }\n }, this.gambifyInput())))\n })), this.tabs());\n }\n tabs() {\n return m(\"div\", {\n className: \"SearchModal-tabs\"\n }, m(\"div\", {\n className: \"SearchModal-tabs-nav\"\n }, this.tabItems().toArray()), m(\"div\", {\n className: \"SearchModal-tabs-content\"\n }, this.activeTabItems().toArray()));\n }\n tabItems() {\n var _this$sources;\n const items = new ItemList();\n (_this$sources = this.sources) == null ? void 0 : _this$sources.map((source, index) => items.add(source.resource, m(Button, {\n className: \"Button Button--link\",\n active: this.activeSource() === source,\n onclick: () => this.switchSource(source)\n }, source.title()), 100 - index));\n return items;\n }\n activeTabItems() {\n var _this$activeSource;\n const items = new ItemList();\n const loading = this.loadingSources.includes(this.activeSource().resource);\n const shouldShowResults = !!this.query() && !loading;\n const gambits = this.gambits();\n const fullPageLink = this.activeSource().fullPage(this.query());\n const results = (_this$activeSource = this.activeSource()) == null ? void 0 : _this$activeSource.view(this.query());\n if (shouldShowResults && fullPageLink) {\n items.add('fullPageLink', m(\"div\", {\n className: \"SearchModal-section\"\n }, m(\"hr\", {\n className: \"Modal-divider\"\n }), m(\"ul\", {\n className: \"Dropdown-menu SearchModal-fullPage\"\n }, fullPageLink)), 80);\n }\n if (!!gambits.length) {\n items.add('gambits', m(\"div\", {\n className: \"SearchModal-section\"\n }, m(\"hr\", {\n className: \"Modal-divider\"\n }), m(\"ul\", {\n className: \"Dropdown-menu SearchModal-options\",\n \"aria-live\": gambits.length ? 'polite' : undefined\n }, m(\"li\", {\n className: \"Dropdown-header\"\n }, app.translator.trans('core.forum.search.options_heading')), gambits)), 60);\n }\n items.add('results', m(\"div\", {\n className: \"SearchModal-section\"\n }, m(\"hr\", {\n className: \"Modal-divider\"\n }), m(\"ul\", {\n className: \"Dropdown-menu SearchModal-results\",\n \"aria-live\": shouldShowResults ? 'polite' : undefined\n }, m(\"li\", {\n className: \"Dropdown-header\"\n }, app.translator.trans('core.forum.search.preview_heading')), !shouldShowResults && m(\"li\", {\n className: \"Dropdown-message\"\n }, m(InfoTile, {\n icon: \"fas fa-search\"\n }, app.translator.trans('core.forum.search.no_search_text'))), shouldShowResults && results, shouldShowResults && !(results != null && results.length) && m(\"li\", {\n className: \"Dropdown-message\"\n }, m(InfoTile, {\n icon: \"far fa-tired\"\n }, app.translator.trans('core.forum.search.no_results_text'))), loading && m(\"li\", {\n className: \"Dropdown-message\"\n }, m(LoadingIndicator, null)))), 40);\n return items;\n }\n switchSource(source) {\n if (this.activeSource() !== source) {\n this.activeSource(source);\n this.search(this.query());\n this.inputElement().focus();\n m.redraw();\n }\n }\n gambits() {\n return this.gambitsAutocomplete[this.activeSource().resource].suggestions(this.query());\n }\n\n /**\n * Transforms a simple search text to wrap valid gambits in a mark tag.\n * @example `lorem ipsum is:unread dolor` => `lorem ipsum is:unread dolor`\n */\n gambifyInput() {\n const query = this.query();\n let marked = query;\n app.search.gambits.match(this.activeSource().resource, query, (gambit, matches, negate, bit) => {\n marked = marked.replace(bit, \"\".concat(bit, \"\"));\n });\n const jsx = [];\n marked.split(/(.*?<\\/mark>)/).forEach(chunk => {\n if (chunk.startsWith('')) {\n jsx.push(m(\"mark\", null, chunk.replace(/<\\/?mark>/g, '')));\n } else {\n jsx.push(chunk);\n }\n });\n return jsx;\n }\n onupdate(vnode) {\n var _this$sources2;\n super.onupdate(vnode);\n\n // Highlight the item that is currently selected.\n this.setIndex(this.getCurrentNumericIndex());\n const component = this;\n this.$('.Dropdown-menu')\n // Whenever the mouse is hovered over a search result, highlight it.\n .on('mouseenter', '> li:not(.Dropdown-header):not(.Dropdown-message)', function () {\n component.setIndex(component.selectableItems().index(this));\n });\n\n // If there are no sources, the search view is not shown.\n if (!((_this$sources2 = this.sources) != null && _this$sources2.length)) return;\n }\n oncreate(vnode) {\n var _this$sources3;\n super.oncreate(vnode);\n\n // If there are no sources, we shouldn't initialize logic for\n // search elements, as they will not be shown.\n if (!((_this$sources3 = this.sources) != null && _this$sources3.length)) return;\n const search = this.search.bind(this);\n\n // Highlight the item that is currently selected.\n this.setIndex(this.getCurrentNumericIndex());\n const $input = this.inputElement();\n this.navigator = new KeyboardNavigatable();\n this.navigator.onUp(() => this.setIndex(this.getCurrentNumericIndex() - 1, true)).onDown(() => this.setIndex(this.getCurrentNumericIndex() + 1, true)).onSelect(this.selectResult.bind(this), true).onCancel(this.clear.bind(this)).bindTo($input);\n\n // Handle input key events on the search input, triggering results to load.\n $input.on('input focus', function () {\n search(this.value.toLowerCase());\n });\n }\n onremove(vnode) {\n this.searchState.setValue(this.query());\n super.onremove(vnode);\n }\n search(query) {\n if (!query) return;\n const source = this.activeSource();\n if (this.searchTimeout) clearTimeout(this.searchTimeout);\n this.searchTimeout = window.setTimeout(() => {\n if (source.isCached(query)) return;\n if (query.length >= SearchManager.MIN_SEARCH_LEN) {\n if (!source.search) return;\n this.loadingSources.push(source.resource);\n source.search(query, SearchModal.LIMIT).then(() => {\n this.loadingSources = this.loadingSources.filter(resource => resource !== source.resource);\n m.redraw();\n });\n }\n this.searchState.cache(query);\n m.redraw();\n }, 250);\n }\n\n /**\n * Navigate to the currently selected search result and close the list.\n */\n selectResult() {\n if (this.searchTimeout) clearTimeout(this.searchTimeout);\n this.loadingSources = [];\n const item = this.getItem(this.index);\n const isResult = !!item.attr('data-id');\n let selectedUrl = null;\n if (isResult) {\n const id = item.attr('data-id');\n selectedUrl = id && this.activeSource().gotoItem(id);\n } else if (item.find('a').length) {\n selectedUrl = item.find('a').attr('href');\n }\n const query = this.query();\n if (query && selectedUrl) {\n m.route.set(selectedUrl);\n } else {\n item.find('button')[0].click();\n }\n }\n\n /**\n * Clear the search\n */\n clear() {\n this.query('');\n }\n\n /**\n * Get all of the search result items that are selectable.\n */\n selectableItems() {\n return this.$('.Dropdown-menu > li:not(.Dropdown-header):not(.Dropdown-message)');\n }\n\n /**\n * Get the position of the currently selected search result item.\n * Returns zero if not found.\n */\n getCurrentNumericIndex() {\n return Math.max(0, this.selectableItems().index(this.getItem(this.index)));\n }\n\n /**\n * Get the
  • in the search results with the given index (numeric or named).\n */\n getItem(index) {\n const $items = this.selectableItems();\n let $item = $items.filter(\"[data-index=\\\"\".concat(index, \"\\\"]\"));\n if (!$item.length) {\n $item = $items.eq(index);\n }\n return $item;\n }\n\n /**\n * Set the currently-selected search result item to the one with the given\n * index.\n */\n setIndex(index, scrollToItem) {\n if (scrollToItem === void 0) {\n scrollToItem = false;\n }\n const $items = this.selectableItems();\n const $dropdown = $items.parent();\n let fixedIndex = index;\n if (index < 0) {\n fixedIndex = $items.length - 1;\n } else if (index >= $items.length) {\n fixedIndex = 0;\n }\n const $item = $items.removeClass('active').eq(fixedIndex).addClass('active');\n this.index = parseInt($item.attr('data-index')) || fixedIndex;\n if (scrollToItem && $dropdown) {\n const dropdownScroll = $dropdown.scrollTop();\n const dropdownTop = $dropdown.offset().top;\n const dropdownBottom = dropdownTop + $dropdown.outerHeight();\n const itemTop = $item.offset().top;\n const itemBottom = itemTop + $item.outerHeight();\n let scrollTop;\n if (itemTop < dropdownTop) {\n scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);\n } else if (itemBottom > dropdownBottom) {\n scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);\n }\n if (typeof scrollTop !== 'undefined') {\n $dropdown.stop(true).animate({\n scrollTop\n }, 100);\n }\n }\n }\n inputElement() {\n return this.$('.SearchModal-input');\n }\n}\n_defineProperty(SearchModal, \"LIMIT\", 6);\nflarum.reg.add('core', 'forum/components/SearchModal', SearchModal);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/SettingsPage.js b/framework/core/js/dist/forum/components/SettingsPage.js index 1371f9fc3f6..ddbbe353c46 100644 --- a/framework/core/js/dist/forum/components/SettingsPage.js +++ b/framework/core/js/dist/forum/components/SettingsPage.js @@ -1,2 +1,551 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[826],{9989:(e,t,s)=>{s.r(t),s.d(t,{default:()=>v});var a=s(7905),n=s(6789),o=s(3390),r=s(4041),i=s(9797),l=s(8312),c=s(8034),d=s(2190),u=s(7298),h=s(9133);class g extends d.Z{oninit(e){super.oninit(e),this.methods=this.notificationMethods().toArray(),this.loading={},this.types=this.notificationTypes().toArray()}view(){const e=this.attrs.user.preferences();return m("table",{className:"NotificationGrid"},m("thead",null,m("tr",null,m("td",null),this.methods.map((e=>m("th",{className:"NotificationGrid-groupToggle",onclick:this.toggleMethod.bind(this,e.name)},m(h.Z,{name:e.icon})," ",e.label))))),m("tbody",null,this.types.map((t=>m("tr",null,m("td",{className:"NotificationGrid-groupToggle",onclick:this.toggleType.bind(this,t.name)},m(h.Z,{name:t.icon})," ",t.label),this.methods.map((s=>{const a=this.preferenceKey(t.name,s.name);return m("td",{className:"NotificationGrid-checkbox"},m(u.Z,{state:!!e[a],loading:this.loading[a],disabled:!(a in e),onchange:this.toggle.bind(this,[a])},m("span",{className:"sr-only"},n.Z.translator.trans("core.forum.settings.notification_checkbox_a11y_label_template",{description:t.label,method:s.label}))))})))))))}oncreate(e){super.oncreate(e),this.$("thead .NotificationGrid-groupToggle").bind("mouseenter mouseleave",(function(e){const t=parseInt($(this).index(),10)+1;$(this).parents("table").find("td:nth-child("+t+")").toggleClass("highlighted","mouseenter"===e.type)})),this.$("tbody .NotificationGrid-groupToggle").bind("mouseenter mouseleave",(function(e){$(this).parent().find("td").toggleClass("highlighted","mouseenter"===e.type)}))}toggle(e){const t=this.attrs.user,s=t.preferences(),a=!s[e[0]];e.forEach((e=>{this.loading[e]=!0,s[e]=a})),m.redraw(),t.save({preferences:s}).then((()=>{e.forEach((e=>this.loading[e]=!1)),m.redraw()}))}toggleMethod(e){const t=this.types.map((t=>this.preferenceKey(t.name,e))).filter((e=>e in this.attrs.user.preferences()));this.toggle(t)}toggleType(e){const t=this.methods.map((t=>this.preferenceKey(e,t.name))).filter((e=>e in this.attrs.user.preferences()));this.toggle(t)}preferenceKey(e,t){return"notify_"+e+"_"+t}notificationMethods(){const e=new r.Z;return e.add("alert",{name:"alert",icon:"fas fa-bell",label:n.Z.translator.trans("core.forum.settings.notify_by_web_heading")}),e.add("email",{name:"email",icon:"far fa-envelope",label:n.Z.translator.trans("core.forum.settings.notify_by_email_heading")}),e}notificationTypes(){const e=new r.Z;return e.add("discussionRenamed",{name:"discussionRenamed",icon:"fas fa-pencil-alt",label:n.Z.translator.trans("core.forum.settings.notify_discussion_renamed_label")}),e}}flarum.reg.add("core","forum/components/NotificationGrid",g);var p=s(899),f=s(6352);class Z extends p.Z{className(){return"ChangePasswordModal Modal--small"}title(){return n.Z.translator.trans("core.forum.change_password.title")}content(){return m("div",{className:"Modal-body"},m(f.Z,{className:"Form--centered"},this.fields().toArray()))}fields(){const e=new r.Z;return e.add("help",m("p",{className:"helpText"},n.Z.translator.trans("core.forum.change_password.text"))),e.add("submit",m("div",{className:"Form-group Form-controls"},m(l.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},n.Z.translator.trans("core.forum.change_password.send_button")))),e}onsubmit(e){e.preventDefault(),this.loading=!0,n.Z.request({method:"POST",url:n.Z.forum.attribute("apiUrl")+"/forgot",body:this.requestBody()}).then(this.hide.bind(this),this.loaded.bind(this))}requestBody(){return{email:n.Z.session.user.email()}}}flarum.reg.add("core","forum/components/ChangePasswordModal",Z);var b=s(6458);class y extends p.Z{constructor(){super(...arguments),(0,a.Z)(this,"email",void 0),(0,a.Z)(this,"password",void 0),(0,a.Z)(this,"success",!1)}oninit(e){super.oninit(e),this.email=(0,b.Z)(n.Z.session.user.email()||""),this.password=(0,b.Z)("")}className(){return"ChangeEmailModal Modal--small"}title(){return n.Z.translator.trans("core.forum.change_email.title")}content(){return m("div",{className:"Modal-body"},m(f.Z,{className:"Form--centered"},this.fields().toArray()))}fields(){const e=new r.Z;return this.success?(e.add("help",m("p",{className:"helpText"},n.Z.translator.trans("core.forum.change_email.confirmation_message",{email:m("strong",null,this.email())}))),e.add("dismiss",m("div",{className:"Form-group"},m(l.Z,{className:"Button Button--primary Button--block",onclick:this.hide.bind(this)},n.Z.translator.trans("core.forum.change_email.dismiss_button"))))):(e.add("email",m("div",{className:"Form-group"},m("input",{type:"email",name:"email",className:"FormControl",placeholder:n.Z.session.user.email(),bidi:this.email,disabled:this.loading}))),e.add("password",m("div",{className:"Form-group"},m("input",{type:"password",name:"password",className:"FormControl",autocomplete:"current-password",placeholder:n.Z.translator.trans("core.forum.change_email.confirm_password_placeholder"),bidi:this.password,disabled:this.loading}))),e.add("submit",m("div",{className:"Form-group Form-controls"},m(l.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},n.Z.translator.trans("core.forum.change_email.submit_button"))))),e}onsubmit(e){e.preventDefault(),this.email()!==n.Z.session.user.email()?(this.loading=!0,this.alertAttrs=null,n.Z.session.user.save(this.requestAttributes(),{errorHandler:this.onerror.bind(this),meta:{password:this.password()}}).then((()=>{this.success=!0})).catch((()=>{})).then(this.loaded.bind(this))):this.hide()}requestAttributes(){return{email:this.email()}}onerror(e){401===e.status&&e.alert&&(e.alert.content=n.Z.translator.trans("core.forum.change_email.incorrect_password_message")),super.onerror(e)}}flarum.reg.add("core","forum/components/ChangeEmailModal",y);var _=s(1268),w=s(1552),N=s(3344);class v extends o.Z{constructor(){super(...arguments),(0,a.Z)(this,"discloseOnlineLoading",void 0)}oninit(e){super.oninit(e),this.show(n.Z.session.user),n.Z.setTitle((0,w.Z)(n.Z.translator.trans("core.forum.settings.title")))}content(){return m("div",{className:"SettingsPage"},m("ul",null,(0,_.Z)(this.settingsItems().toArray())))}settingsItems(){const e=new r.Z;return["account","notifications","privacy"].forEach(((t,s)=>{const a="".concat(t,"Items");e.add(t,m(c.Z,{className:(0,N.Z)("Settings-".concat(t),{"FieldSet--col":"account"===t}),label:n.Z.translator.trans("core.forum.settings.".concat(t,"_heading"))},this[a]().toArray()),100-10*s)})),e}accountItems(){const e=new r.Z;return e.add("changePassword",m(l.Z,{className:"Button",onclick:()=>n.Z.modal.show(Z)},n.Z.translator.trans("core.forum.settings.change_password_button")),100),e.add("changeEmail",m(l.Z,{className:"Button",onclick:()=>n.Z.modal.show(y)},n.Z.translator.trans("core.forum.settings.change_email_button")),90),e}notificationsItems(){const e=new r.Z;return e.add("notificationGrid",m(g,{user:this.user}),100),e}privacyItems(){var e;const t=new r.Z;return t.add("discloseOnline",m(i.Z,{state:null==(e=this.user.preferences())?void 0:e.discloseOnline,onchange:e=>{this.discloseOnlineLoading=!0,this.user.savePreferences({discloseOnline:e}).then((()=>{this.discloseOnlineLoading=!1,m.redraw()}))},loading:this.discloseOnlineLoading},n.Z.translator.trans("core.forum.settings.privacy_disclose_online_label")),100),t}}flarum.reg.add("core","forum/components/SettingsPage",v)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/SettingsPage"],{ + +/***/ "./src/forum/components/ChangeEmailModal.tsx": +/*!***************************************************!*\ + !*** ./src/forum/components/ChangeEmailModal.tsx ***! + \***************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ChangeEmailModal) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_components_Form__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/components/Form */ "./src/common/components/Form.tsx"); + + + + + + + + +/** + * The `ChangeEmailModal` component shows a modal dialog which allows the user + * to change their email address. + */ +class ChangeEmailModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + /** + * The value of the email input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "email", void 0); + /** + * The value of the password input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "password", void 0); + /** + * Whether or not the email has been changed successfully. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "success", false); + } + oninit(vnode) { + super.oninit(vnode); + this.email = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_4__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user.email() || ''); + this.password = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_4__["default"])(''); + } + className() { + return 'ChangeEmailModal Modal--small'; + } + title() { + return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.change_email.title'); + } + content() { + return m("div", { + className: "Modal-body" + }, m(_common_components_Form__WEBPACK_IMPORTED_MODULE_6__["default"], { + className: "Form--centered" + }, this.fields().toArray())); + } + fields() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"](); + if (this.success) { + items.add('help', m("p", { + className: "helpText" + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.change_email.confirmation_message', { + email: m("strong", null, this.email()) + }))); + items.add('dismiss', m("div", { + className: "Form-group" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary Button--block", + onclick: this.hide.bind(this) + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.change_email.dismiss_button')))); + } else { + items.add('email', m("div", { + className: "Form-group" + }, m("input", { + type: "email", + name: "email", + className: "FormControl", + placeholder: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user.email(), + bidi: this.email, + disabled: this.loading + }))); + items.add('password', m("div", { + className: "Form-group" + }, m("input", { + type: "password", + name: "password", + className: "FormControl", + autocomplete: "current-password", + placeholder: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.change_email.confirm_password_placeholder'), + bidi: this.password, + disabled: this.loading + }))); + items.add('submit', m("div", { + className: "Form-group Form-controls" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary Button--block", + type: "submit", + loading: this.loading + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.change_email.submit_button')))); + } + return items; + } + onsubmit(e) { + e.preventDefault(); + + // If the user hasn't actually entered a different email address, we don't + // need to do anything. Woot! + if (this.email() === _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user.email()) { + this.hide(); + return; + } + this.loading = true; + this.alertAttrs = null; + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user.save(this.requestAttributes(), { + errorHandler: this.onerror.bind(this), + meta: { + password: this.password() + } + }).then(() => { + this.success = true; + }).catch(() => {}).then(this.loaded.bind(this)); + } + requestAttributes() { + return { + email: this.email() + }; + } + onerror(error) { + if (error.status === 401 && error.alert) { + error.alert.content = _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.change_email.incorrect_password_message'); + } + super.onerror(error); + } +} +flarum.reg.add('core', 'forum/components/ChangeEmailModal', ChangeEmailModal); + +/***/ }), + +/***/ "./src/forum/components/ChangePasswordModal.tsx": +/*!******************************************************!*\ + !*** ./src/forum/components/ChangePasswordModal.tsx ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ ChangePasswordModal) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_components_Form__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/components/Form */ "./src/common/components/Form.tsx"); + + + + + + +/** + * The `ChangePasswordModal` component shows a modal dialog which allows the + * user to send themself a password reset email. + */ +class ChangePasswordModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_1__["default"] { + className() { + return 'ChangePasswordModal Modal--small'; + } + title() { + return _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.change_password.title'); + } + content() { + return m("div", { + className: "Modal-body" + }, m(_common_components_Form__WEBPACK_IMPORTED_MODULE_4__["default"], { + className: "Form--centered" + }, this.fields().toArray())); + } + fields() { + const fields = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + fields.add('help', m("p", { + className: "helpText" + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.change_password.text'))); + fields.add('submit', m("div", { + className: "Form-group Form-controls" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_2__["default"], { + className: "Button Button--primary Button--block", + type: "submit", + loading: this.loading + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.change_password.send_button')))); + return fields; + } + onsubmit(e) { + e.preventDefault(); + this.loading = true; + _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].request({ + method: 'POST', + url: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].forum.attribute('apiUrl') + '/forgot', + body: this.requestBody() + }).then(this.hide.bind(this), this.loaded.bind(this)); + } + requestBody() { + return { + email: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].session.user.email() + }; + } +} +flarum.reg.add('core', 'forum/components/ChangePasswordModal', ChangePasswordModal); + +/***/ }), + +/***/ "./src/forum/components/SettingsPage.tsx": +/*!***********************************************!*\ + !*** ./src/forum/components/SettingsPage.tsx ***! + \***********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ SettingsPage) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _UserPage__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./UserPage */ "./src/forum/components/UserPage.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_components_Switch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/components/Switch */ "./src/common/components/Switch.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_components_FieldSet__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/components/FieldSet */ "./src/common/components/FieldSet.tsx"); +/* harmony import */ var _NotificationGrid__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./NotificationGrid */ "./src/forum/components/NotificationGrid.js"); +/* harmony import */ var _ChangePasswordModal__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./ChangePasswordModal */ "./src/forum/components/ChangePasswordModal.tsx"); +/* harmony import */ var _ChangeEmailModal__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./ChangeEmailModal */ "./src/forum/components/ChangeEmailModal.tsx"); +/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts"); + + + + + + + + + + + + + + +/** + * The `SettingsPage` component displays the user's settings control panel, in + * the context of their user profile. + */ +class SettingsPage extends _UserPage__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "discloseOnlineLoading", void 0); + } + oninit(vnode) { + super.oninit(vnode); + this.show(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user); + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].setTitle((0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_11__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.settings.title'))); + } + content() { + return m("div", { + className: "SettingsPage" + }, m("ul", null, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_10__["default"])(this.settingsItems().toArray()))); + } + + /** + * Build an item list for the user's settings controls. + */ + settingsItems() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + ['account', 'notifications', 'privacy'].forEach((section, index) => { + const sectionItems = "".concat(section, "Items"); + items.add(section, m(_common_components_FieldSet__WEBPACK_IMPORTED_MODULE_6__["default"], { + className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_12__["default"])("Settings-".concat(section), { + 'FieldSet--col': section === 'account' + }), + label: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans("core.forum.settings.".concat(section, "_heading")) + }, this[sectionItems]().toArray()), 100 - index * 10); + }); + return items; + } + + /** + * Build an item list for the user's account settings. + */ + accountItems() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + items.add('changePassword', m(_common_components_Button__WEBPACK_IMPORTED_MODULE_5__["default"], { + className: "Button", + onclick: () => _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(_ChangePasswordModal__WEBPACK_IMPORTED_MODULE_8__["default"]) + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.settings.change_password_button')), 100); + items.add('changeEmail', m(_common_components_Button__WEBPACK_IMPORTED_MODULE_5__["default"], { + className: "Button", + onclick: () => _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(_ChangeEmailModal__WEBPACK_IMPORTED_MODULE_9__["default"]) + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.settings.change_email_button')), 90); + return items; + } + + /** + * Build an item list for the user's notification settings. + */ + notificationsItems() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + items.add('notificationGrid', m(_NotificationGrid__WEBPACK_IMPORTED_MODULE_7__["default"], { + user: this.user + }), 100); + return items; + } + + /** + * Build an item list for the user's privacy settings. + */ + privacyItems() { + var _preferences; + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + items.add('discloseOnline', m(_common_components_Switch__WEBPACK_IMPORTED_MODULE_4__["default"], { + state: (_preferences = this.user.preferences()) == null ? void 0 : _preferences.discloseOnline, + onchange: value => { + this.discloseOnlineLoading = true; + this.user.savePreferences({ + discloseOnline: value + }).then(() => { + this.discloseOnlineLoading = false; + m.redraw(); + }); + }, + loading: this.discloseOnlineLoading + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.settings.privacy_disclose_online_label')), 100); + return items; + } +} +flarum.reg.add('core', 'forum/components/SettingsPage', SettingsPage); + +/***/ }), + +/***/ "./src/forum/components/NotificationGrid.js": +/*!**************************************************!*\ + !*** ./src/forum/components/NotificationGrid.js ***! + \**************************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ NotificationGrid) +/* harmony export */ }); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_components_Checkbox__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/Checkbox */ "./src/common/components/Checkbox.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_components_Icon__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/components/Icon */ "./src/common/components/Icon.tsx"); + + + + + + +/** + * The `NotificationGrid` component displays a table of notification types and + * methods, allowing the user to toggle each combination. + * + * ### Attrs + * + * - `user` + */ +class NotificationGrid extends _common_Component__WEBPACK_IMPORTED_MODULE_1__["default"] { + oninit(vnode) { + super.oninit(vnode); + + /** + * Information about the available notification methods. + * + * @type {({ name: string, icon: string, label: import('mithril').Children })[]} + */ + this.methods = this.notificationMethods().toArray(); + + /** + * A map of which notification checkboxes are loading. + * + * @type {Record} + */ + this.loading = {}; + + /** + * Information about the available notification types. + * + * @type {({ name: string, icon: string, label: import('mithril').Children })[]} + */ + this.types = this.notificationTypes().toArray(); + } + view() { + const preferences = this.attrs.user.preferences(); + return m("table", { + className: "NotificationGrid" + }, m("thead", null, m("tr", null, m("td", null), this.methods.map(method => m("th", { + className: "NotificationGrid-groupToggle", + onclick: this.toggleMethod.bind(this, method.name) + }, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_4__["default"], { + name: method.icon + }), " ", method.label)))), m("tbody", null, this.types.map(type => m("tr", null, m("td", { + className: "NotificationGrid-groupToggle", + onclick: this.toggleType.bind(this, type.name) + }, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_4__["default"], { + name: type.icon + }), " ", type.label), this.methods.map(method => { + const key = this.preferenceKey(type.name, method.name); + return m("td", { + className: "NotificationGrid-checkbox" + }, m(_common_components_Checkbox__WEBPACK_IMPORTED_MODULE_2__["default"], { + state: !!preferences[key], + loading: this.loading[key], + disabled: !(key in preferences), + onchange: this.toggle.bind(this, [key]) + }, m("span", { + className: "sr-only" + }, _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.settings.notification_checkbox_a11y_label_template', { + description: type.label, + method: method.label + })))); + }))))); + } + oncreate(vnode) { + super.oncreate(vnode); + this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) { + const i = parseInt($(this).index(), 10) + 1; + $(this).parents('table').find('td:nth-child(' + i + ')').toggleClass('highlighted', e.type === 'mouseenter'); + }); + this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) { + $(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter'); + }); + } + + /** + * Toggle the state of the given preferences, based on the value of the first + * one. + * + * @param {string[]} keys + */ + toggle(keys) { + const user = this.attrs.user; + const preferences = user.preferences(); + const enabled = !preferences[keys[0]]; + keys.forEach(key => { + this.loading[key] = true; + preferences[key] = enabled; + }); + m.redraw(); + user.save({ + preferences + }).then(() => { + keys.forEach(key => this.loading[key] = false); + m.redraw(); + }); + } + + /** + * Toggle all notification types for the given method. + * + * @param {string} method + */ + toggleMethod(method) { + const keys = this.types.map(type => this.preferenceKey(type.name, method)).filter(key => key in this.attrs.user.preferences()); + this.toggle(keys); + } + + /** + * Toggle all notification methods for the given type. + * + * @param {string} type + */ + toggleType(type) { + const keys = this.methods.map(method => this.preferenceKey(type, method.name)).filter(key => key in this.attrs.user.preferences()); + this.toggle(keys); + } + + /** + * Get the name of the preference key for the given notification type-method + * combination. + * + * @param {string} type + * @param {string} method + * @return {string} + */ + preferenceKey(type, method) { + return 'notify_' + type + '_' + method; + } + + /** + * Build an item list for the notification methods to display in the grid. + * + * Each notification method is an object which has the following properties: + * + * - `name` The name of the notification method. + * - `icon` The icon to display in the column header. + * - `label` The label to display in the column header. + * + * @return {ItemList<{ name: string, icon: string, label: import('mithril').Children }>} + */ + notificationMethods() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + items.add('alert', { + name: 'alert', + icon: 'fas fa-bell', + label: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.settings.notify_by_web_heading') + }); + items.add('email', { + name: 'email', + icon: 'far fa-envelope', + label: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.settings.notify_by_email_heading') + }); + return items; + } + + /** + * Build an item list for the notification types to display in the grid. + * + * Each notification type is an object which has the following properties: + * + * - `name` The name of the notification type. + * - `icon` The icon to display in the notification grid row. + * - `label` The label to display in the notification grid row. + * + * @return {ItemList<{ name: string, icon: string, label: import('mithril').Children}>} + */ + notificationTypes() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + items.add('discussionRenamed', { + name: 'discussionRenamed', + icon: 'fas fa-pencil-alt', + label: _forum_app__WEBPACK_IMPORTED_MODULE_0__["default"].translator.trans('core.forum.settings.notify_discussion_renamed_label') + }); + return items; + } +} +flarum.reg.add('core', 'forum/components/NotificationGrid', NotificationGrid); + +/***/ }) + +}]); //# sourceMappingURL=SettingsPage.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/SettingsPage.js.map b/framework/core/js/dist/forum/components/SettingsPage.js.map index f4c4c71ead4..90e0463e270 100644 --- a/framework/core/js/dist/forum/components/SettingsPage.js.map +++ b/framework/core/js/dist/forum/components/SettingsPage.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/SettingsPage.js","mappings":"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","sources":["webpack://@flarum/core/./src/forum/components/NotificationGrid.js","webpack://@flarum/core/./src/forum/components/ChangePasswordModal.tsx","webpack://@flarum/core/./src/forum/components/ChangeEmailModal.tsx","webpack://@flarum/core/./src/forum/components/SettingsPage.tsx"],"sourcesContent":["import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport Checkbox from '../../common/components/Checkbox';\nimport ItemList from '../../common/utils/ItemList';\nimport Icon from '../../common/components/Icon';\n\n/**\n * The `NotificationGrid` component displays a table of notification types and\n * methods, allowing the user to toggle each combination.\n *\n * ### Attrs\n *\n * - `user`\n */\nexport default class NotificationGrid extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n\n /**\n * Information about the available notification methods.\n *\n * @type {({ name: string, icon: string, label: import('mithril').Children })[]}\n */\n this.methods = this.notificationMethods().toArray();\n\n /**\n * A map of which notification checkboxes are loading.\n *\n * @type {Record}\n */\n this.loading = {};\n\n /**\n * Information about the available notification types.\n *\n * @type {({ name: string, icon: string, label: import('mithril').Children })[]}\n */\n this.types = this.notificationTypes().toArray();\n }\n view() {\n const preferences = this.attrs.user.preferences();\n return m(\"table\", {\n className: \"NotificationGrid\"\n }, m(\"thead\", null, m(\"tr\", null, m(\"td\", null), this.methods.map(method => m(\"th\", {\n className: \"NotificationGrid-groupToggle\",\n onclick: this.toggleMethod.bind(this, method.name)\n }, m(Icon, {\n name: method.icon\n }), \" \", method.label)))), m(\"tbody\", null, this.types.map(type => m(\"tr\", null, m(\"td\", {\n className: \"NotificationGrid-groupToggle\",\n onclick: this.toggleType.bind(this, type.name)\n }, m(Icon, {\n name: type.icon\n }), \" \", type.label), this.methods.map(method => {\n const key = this.preferenceKey(type.name, method.name);\n return m(\"td\", {\n className: \"NotificationGrid-checkbox\"\n }, m(Checkbox, {\n state: !!preferences[key],\n loading: this.loading[key],\n disabled: !(key in preferences),\n onchange: this.toggle.bind(this, [key])\n }, m(\"span\", {\n className: \"sr-only\"\n }, app.translator.trans('core.forum.settings.notification_checkbox_a11y_label_template', {\n description: type.label,\n method: method.label\n }))));\n })))));\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {\n const i = parseInt($(this).index(), 10) + 1;\n $(this).parents('table').find('td:nth-child(' + i + ')').toggleClass('highlighted', e.type === 'mouseenter');\n });\n this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {\n $(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter');\n });\n }\n\n /**\n * Toggle the state of the given preferences, based on the value of the first\n * one.\n *\n * @param {string[]} keys\n */\n toggle(keys) {\n const user = this.attrs.user;\n const preferences = user.preferences();\n const enabled = !preferences[keys[0]];\n keys.forEach(key => {\n this.loading[key] = true;\n preferences[key] = enabled;\n });\n m.redraw();\n user.save({\n preferences\n }).then(() => {\n keys.forEach(key => this.loading[key] = false);\n m.redraw();\n });\n }\n\n /**\n * Toggle all notification types for the given method.\n *\n * @param {string} method\n */\n toggleMethod(method) {\n const keys = this.types.map(type => this.preferenceKey(type.name, method)).filter(key => key in this.attrs.user.preferences());\n this.toggle(keys);\n }\n\n /**\n * Toggle all notification methods for the given type.\n *\n * @param {string} type\n */\n toggleType(type) {\n const keys = this.methods.map(method => this.preferenceKey(type, method.name)).filter(key => key in this.attrs.user.preferences());\n this.toggle(keys);\n }\n\n /**\n * Get the name of the preference key for the given notification type-method\n * combination.\n *\n * @param {string} type\n * @param {string} method\n * @return {string}\n */\n preferenceKey(type, method) {\n return 'notify_' + type + '_' + method;\n }\n\n /**\n * Build an item list for the notification methods to display in the grid.\n *\n * Each notification method is an object which has the following properties:\n *\n * - `name` The name of the notification method.\n * - `icon` The icon to display in the column header.\n * - `label` The label to display in the column header.\n *\n * @return {ItemList<{ name: string, icon: string, label: import('mithril').Children }>}\n */\n notificationMethods() {\n const items = new ItemList();\n items.add('alert', {\n name: 'alert',\n icon: 'fas fa-bell',\n label: app.translator.trans('core.forum.settings.notify_by_web_heading')\n });\n items.add('email', {\n name: 'email',\n icon: 'far fa-envelope',\n label: app.translator.trans('core.forum.settings.notify_by_email_heading')\n });\n return items;\n }\n\n /**\n * Build an item list for the notification types to display in the grid.\n *\n * Each notification type is an object which has the following properties:\n *\n * - `name` The name of the notification type.\n * - `icon` The icon to display in the notification grid row.\n * - `label` The label to display in the notification grid row.\n *\n * @return {ItemList<{ name: string, icon: string, label: import('mithril').Children}>}\n */\n notificationTypes() {\n const items = new ItemList();\n items.add('discussionRenamed', {\n name: 'discussionRenamed',\n icon: 'fas fa-pencil-alt',\n label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label')\n });\n return items;\n }\n}\nflarum.reg.add('core', 'forum/components/NotificationGrid', NotificationGrid);","import app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport ItemList from '../../common/utils/ItemList';\nimport Form from '../../common/components/Form';\n\n/**\n * The `ChangePasswordModal` component shows a modal dialog which allows the\n * user to send themself a password reset email.\n */\nexport default class ChangePasswordModal extends FormModal {\n className() {\n return 'ChangePasswordModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.change_password.title');\n }\n content() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, this.fields().toArray()));\n }\n fields() {\n const fields = new ItemList();\n fields.add('help', m(\"p\", {\n className: \"helpText\"\n }, app.translator.trans('core.forum.change_password.text')));\n fields.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.change_password.send_button'))));\n return fields;\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n app.request({\n method: 'POST',\n url: app.forum.attribute('apiUrl') + '/forgot',\n body: this.requestBody()\n }).then(this.hide.bind(this), this.loaded.bind(this));\n }\n requestBody() {\n return {\n email: app.session.user.email()\n };\n }\n}\nflarum.reg.add('core', 'forum/components/ChangePasswordModal', ChangePasswordModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport Stream from '../../common/utils/Stream';\nimport ItemList from '../../common/utils/ItemList';\nimport Form from '../../common/components/Form';\n\n/**\n * The `ChangeEmailModal` component shows a modal dialog which allows the user\n * to change their email address.\n */\nexport default class ChangeEmailModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the email input.\n */\n _defineProperty(this, \"email\", void 0);\n /**\n * The value of the password input.\n */\n _defineProperty(this, \"password\", void 0);\n /**\n * Whether or not the email has been changed successfully.\n */\n _defineProperty(this, \"success\", false);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.email = Stream(app.session.user.email() || '');\n this.password = Stream('');\n }\n className() {\n return 'ChangeEmailModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.change_email.title');\n }\n content() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, this.fields().toArray()));\n }\n fields() {\n const items = new ItemList();\n if (this.success) {\n items.add('help', m(\"p\", {\n className: \"helpText\"\n }, app.translator.trans('core.forum.change_email.confirmation_message', {\n email: m(\"strong\", null, this.email())\n })));\n items.add('dismiss', m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n onclick: this.hide.bind(this)\n }, app.translator.trans('core.forum.change_email.dismiss_button'))));\n } else {\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n type: \"email\",\n name: \"email\",\n className: \"FormControl\",\n placeholder: app.session.user.email(),\n bidi: this.email,\n disabled: this.loading\n })));\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n type: \"password\",\n name: \"password\",\n className: \"FormControl\",\n autocomplete: \"current-password\",\n placeholder: app.translator.trans('core.forum.change_email.confirm_password_placeholder'),\n bidi: this.password,\n disabled: this.loading\n })));\n items.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.change_email.submit_button'))));\n }\n return items;\n }\n onsubmit(e) {\n e.preventDefault();\n\n // If the user hasn't actually entered a different email address, we don't\n // need to do anything. Woot!\n if (this.email() === app.session.user.email()) {\n this.hide();\n return;\n }\n this.loading = true;\n this.alertAttrs = null;\n app.session.user.save(this.requestAttributes(), {\n errorHandler: this.onerror.bind(this),\n meta: {\n password: this.password()\n }\n }).then(() => {\n this.success = true;\n }).catch(() => {}).then(this.loaded.bind(this));\n }\n requestAttributes() {\n return {\n email: this.email()\n };\n }\n onerror(error) {\n if (error.status === 401 && error.alert) {\n error.alert.content = app.translator.trans('core.forum.change_email.incorrect_password_message');\n }\n super.onerror(error);\n }\n}\nflarum.reg.add('core', 'forum/components/ChangeEmailModal', ChangeEmailModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport UserPage from './UserPage';\nimport ItemList from '../../common/utils/ItemList';\nimport Switch from '../../common/components/Switch';\nimport Button from '../../common/components/Button';\nimport FieldSet from '../../common/components/FieldSet';\nimport NotificationGrid from './NotificationGrid';\nimport ChangePasswordModal from './ChangePasswordModal';\nimport ChangeEmailModal from './ChangeEmailModal';\nimport listItems from '../../common/helpers/listItems';\nimport extractText from '../../common/utils/extractText';\nimport classList from '../../common/utils/classList';\n\n/**\n * The `SettingsPage` component displays the user's settings control panel, in\n * the context of their user profile.\n */\nexport default class SettingsPage extends UserPage {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"discloseOnlineLoading\", void 0);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.show(app.session.user);\n app.setTitle(extractText(app.translator.trans('core.forum.settings.title')));\n }\n content() {\n return m(\"div\", {\n className: \"SettingsPage\"\n }, m(\"ul\", null, listItems(this.settingsItems().toArray())));\n }\n\n /**\n * Build an item list for the user's settings controls.\n */\n settingsItems() {\n const items = new ItemList();\n ['account', 'notifications', 'privacy'].forEach((section, index) => {\n const sectionItems = \"\".concat(section, \"Items\");\n items.add(section, m(FieldSet, {\n className: classList(\"Settings-\".concat(section), {\n 'FieldSet--col': section === 'account'\n }),\n label: app.translator.trans(\"core.forum.settings.\".concat(section, \"_heading\"))\n }, this[sectionItems]().toArray()), 100 - index * 10);\n });\n return items;\n }\n\n /**\n * Build an item list for the user's account settings.\n */\n accountItems() {\n const items = new ItemList();\n items.add('changePassword', m(Button, {\n className: \"Button\",\n onclick: () => app.modal.show(ChangePasswordModal)\n }, app.translator.trans('core.forum.settings.change_password_button')), 100);\n items.add('changeEmail', m(Button, {\n className: \"Button\",\n onclick: () => app.modal.show(ChangeEmailModal)\n }, app.translator.trans('core.forum.settings.change_email_button')), 90);\n return items;\n }\n\n /**\n * Build an item list for the user's notification settings.\n */\n notificationsItems() {\n const items = new ItemList();\n items.add('notificationGrid', m(NotificationGrid, {\n user: this.user\n }), 100);\n return items;\n }\n\n /**\n * Build an item list for the user's privacy settings.\n */\n privacyItems() {\n var _preferences;\n const items = new ItemList();\n items.add('discloseOnline', m(Switch, {\n state: (_preferences = this.user.preferences()) == null ? void 0 : _preferences.discloseOnline,\n onchange: value => {\n this.discloseOnlineLoading = true;\n this.user.savePreferences({\n discloseOnline: value\n }).then(() => {\n this.discloseOnlineLoading = false;\n m.redraw();\n });\n },\n loading: this.discloseOnlineLoading\n }, app.translator.trans('core.forum.settings.privacy_disclose_online_label')), 100);\n return items;\n }\n}\nflarum.reg.add('core', 'forum/components/SettingsPage', SettingsPage);"],"names":["NotificationGrid","Component","oninit","vnode","super","this","methods","notificationMethods","toArray","loading","types","notificationTypes","view","preferences","attrs","user","m","className","map","method","onclick","toggleMethod","bind","name","Icon","icon","label","type","toggleType","key","preferenceKey","Checkbox","state","disabled","onchange","toggle","app","description","oncreate","$","e","i","parseInt","index","parents","find","toggleClass","parent","keys","enabled","forEach","redraw","save","then","filter","items","ItemList","add","flarum","reg","ChangePasswordModal","FormModal","title","content","Form","fields","Button","onsubmit","preventDefault","url","body","requestBody","hide","loaded","email","ChangeEmailModal","constructor","arguments","Stream","password","success","placeholder","bidi","autocomplete","alertAttrs","requestAttributes","errorHandler","onerror","meta","catch","error","status","alert","SettingsPage","UserPage","show","extractText","listItems","settingsItems","section","sectionItems","concat","FieldSet","classList","accountItems","notificationsItems","privacyItems","_preferences","Switch","discloseOnline","value","discloseOnlineLoading","savePreferences"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/SettingsPage.js","mappings":";;;;;;;;;;;;;;;;;;;;AAAwE;AACtC;AACwB;AACN;AACL;AACI;AACH;;AAEhD;AACA;AACA;AACA;AACe,+BAA+B,oEAAS;AACvD;AACA;AACA;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,iBAAiB,gEAAM,CAAC,qEAAsB;AAC9C,oBAAoB,gEAAM;AAC1B;AACA;AACA;AACA;AACA;AACA,WAAW,mEAAoB;AAC/B;AACA;AACA;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,KAAK;AACL;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA,OAAO;AACP;AACA;AACA,OAAO,IAAI,iEAAM;AACjB;AACA;AACA,OAAO,EAAE,mEAAoB;AAC7B,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,qBAAqB,qEAAsB;AAC3C;AACA;AACA,OAAO;AACP;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA,qBAAqB,mEAAoB;AACzC;AACA;AACA,OAAO;AACP;AACA;AACA,OAAO,IAAI,iEAAM;AACjB;AACA;AACA;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,yBAAyB,qEAAsB;AAC/C;AACA;AACA;AACA;AACA;AACA,IAAI,oEAAqB;AACzB;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,mEAAoB;AAChD;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;AC5HkC;AACwB;AACN;AACD;AACH;;AAEhD;AACA;AACA;AACA;AACe,kCAAkC,oEAAS;AAC1D;AACA;AACA;AACA;AACA,WAAW,mEAAoB;AAC/B;AACA;AACA;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,KAAK;AACL;AACA;AACA,uBAAuB,8DAAQ;AAC/B;AACA;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA,KAAK,IAAI,iEAAM;AACf;AACA;AACA;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA;AACA;AACA;AACA,IAAI,0DAAW;AACf;AACA,WAAW,kEAAmB;AAC9B;AACA,KAAK;AACL;AACA;AACA;AACA,aAAa,qEAAsB;AACnC;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrDwE;AACtC;AACA;AACiB;AACC;AACA;AACI;AACN;AACM;AACN;AACK;AACE;AACJ;;AAErD;AACA;AACA;AACA;AACe,2BAA2B,iDAAQ;AAClD;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,cAAc,+DAAgB;AAC9B,IAAI,2DAAY,CAAC,sEAAW,CAAC,mEAAoB;AACjD;AACA;AACA;AACA;AACA,KAAK,gBAAgB,sEAAS;AAC9B;;AAEA;AACA;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA,2BAA2B,mEAAQ;AACnC,mBAAmB,oEAAS;AAC5B;AACA,SAAS;AACT,eAAe,mEAAoB;AACnC,OAAO;AACP,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B,kCAAkC,iEAAM;AACxC;AACA,qBAAqB,6DAAc,CAAC,4DAAmB;AACvD,KAAK,EAAE,mEAAoB;AAC3B,+BAA+B,iEAAM;AACrC;AACA,qBAAqB,6DAAc,CAAC,yDAAgB;AACpD,KAAK,EAAE,mEAAoB;AAC3B;AACA;;AAEA;AACA;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B,oCAAoC,yDAAgB;AACpD;AACA,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B,kCAAkC,iEAAM;AACxC;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA;AACA,SAAS;AACT,OAAO;AACP;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;ACpGkC;AACa;AACS;AACL;AACH;;AAEhD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,+BAA+B,yDAAS;AACvD;AACA;;AAEA;AACA;AACA;AACA,cAAc,GAAG,+DAA+D;AAChF;AACA;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;;AAEA;AACA;AACA;AACA,cAAc,GAAG,+DAA+D;AAChF;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,KAAK;AACL;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,KAAK;AACL;AACA;AACA;AACA,OAAO,IAAI,mEAAQ;AACnB;AACA;AACA;AACA;AACA,OAAO;AACP;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA;AACA,OAAO;AACP,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA,aAAa,UAAU;AACvB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,aAAa,QAAQ;AACrB;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,aAAa,QAAQ;AACrB,aAAa,QAAQ;AACrB,cAAc;AACd;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,WAAW,+DAA+D;AACxF;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA;AACA,aAAa,mEAAoB;AACjC,KAAK;AACL;AACA;AACA;AACA,aAAa,mEAAoB;AACjC,KAAK;AACL;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,WAAW,8DAA8D;AACvF;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA;AACA,aAAa,mEAAoB;AACjC,KAAK;AACL;AACA;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/ChangeEmailModal.tsx","webpack://@flarum/core/./src/forum/components/ChangePasswordModal.tsx","webpack://@flarum/core/./src/forum/components/SettingsPage.tsx","webpack://@flarum/core/./src/forum/components/NotificationGrid.js"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport Stream from '../../common/utils/Stream';\nimport ItemList from '../../common/utils/ItemList';\nimport Form from '../../common/components/Form';\n\n/**\n * The `ChangeEmailModal` component shows a modal dialog which allows the user\n * to change their email address.\n */\nexport default class ChangeEmailModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the email input.\n */\n _defineProperty(this, \"email\", void 0);\n /**\n * The value of the password input.\n */\n _defineProperty(this, \"password\", void 0);\n /**\n * Whether or not the email has been changed successfully.\n */\n _defineProperty(this, \"success\", false);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.email = Stream(app.session.user.email() || '');\n this.password = Stream('');\n }\n className() {\n return 'ChangeEmailModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.change_email.title');\n }\n content() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, this.fields().toArray()));\n }\n fields() {\n const items = new ItemList();\n if (this.success) {\n items.add('help', m(\"p\", {\n className: \"helpText\"\n }, app.translator.trans('core.forum.change_email.confirmation_message', {\n email: m(\"strong\", null, this.email())\n })));\n items.add('dismiss', m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n onclick: this.hide.bind(this)\n }, app.translator.trans('core.forum.change_email.dismiss_button'))));\n } else {\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n type: \"email\",\n name: \"email\",\n className: \"FormControl\",\n placeholder: app.session.user.email(),\n bidi: this.email,\n disabled: this.loading\n })));\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n type: \"password\",\n name: \"password\",\n className: \"FormControl\",\n autocomplete: \"current-password\",\n placeholder: app.translator.trans('core.forum.change_email.confirm_password_placeholder'),\n bidi: this.password,\n disabled: this.loading\n })));\n items.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.change_email.submit_button'))));\n }\n return items;\n }\n onsubmit(e) {\n e.preventDefault();\n\n // If the user hasn't actually entered a different email address, we don't\n // need to do anything. Woot!\n if (this.email() === app.session.user.email()) {\n this.hide();\n return;\n }\n this.loading = true;\n this.alertAttrs = null;\n app.session.user.save(this.requestAttributes(), {\n errorHandler: this.onerror.bind(this),\n meta: {\n password: this.password()\n }\n }).then(() => {\n this.success = true;\n }).catch(() => {}).then(this.loaded.bind(this));\n }\n requestAttributes() {\n return {\n email: this.email()\n };\n }\n onerror(error) {\n if (error.status === 401 && error.alert) {\n error.alert.content = app.translator.trans('core.forum.change_email.incorrect_password_message');\n }\n super.onerror(error);\n }\n}\nflarum.reg.add('core', 'forum/components/ChangeEmailModal', ChangeEmailModal);","import app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport ItemList from '../../common/utils/ItemList';\nimport Form from '../../common/components/Form';\n\n/**\n * The `ChangePasswordModal` component shows a modal dialog which allows the\n * user to send themself a password reset email.\n */\nexport default class ChangePasswordModal extends FormModal {\n className() {\n return 'ChangePasswordModal Modal--small';\n }\n title() {\n return app.translator.trans('core.forum.change_password.title');\n }\n content() {\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, this.fields().toArray()));\n }\n fields() {\n const fields = new ItemList();\n fields.add('help', m(\"p\", {\n className: \"helpText\"\n }, app.translator.trans('core.forum.change_password.text')));\n fields.add('submit', m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.change_password.send_button'))));\n return fields;\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n app.request({\n method: 'POST',\n url: app.forum.attribute('apiUrl') + '/forgot',\n body: this.requestBody()\n }).then(this.hide.bind(this), this.loaded.bind(this));\n }\n requestBody() {\n return {\n email: app.session.user.email()\n };\n }\n}\nflarum.reg.add('core', 'forum/components/ChangePasswordModal', ChangePasswordModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport UserPage from './UserPage';\nimport ItemList from '../../common/utils/ItemList';\nimport Switch from '../../common/components/Switch';\nimport Button from '../../common/components/Button';\nimport FieldSet from '../../common/components/FieldSet';\nimport NotificationGrid from './NotificationGrid';\nimport ChangePasswordModal from './ChangePasswordModal';\nimport ChangeEmailModal from './ChangeEmailModal';\nimport listItems from '../../common/helpers/listItems';\nimport extractText from '../../common/utils/extractText';\nimport classList from '../../common/utils/classList';\n\n/**\n * The `SettingsPage` component displays the user's settings control panel, in\n * the context of their user profile.\n */\nexport default class SettingsPage extends UserPage {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"discloseOnlineLoading\", void 0);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.show(app.session.user);\n app.setTitle(extractText(app.translator.trans('core.forum.settings.title')));\n }\n content() {\n return m(\"div\", {\n className: \"SettingsPage\"\n }, m(\"ul\", null, listItems(this.settingsItems().toArray())));\n }\n\n /**\n * Build an item list for the user's settings controls.\n */\n settingsItems() {\n const items = new ItemList();\n ['account', 'notifications', 'privacy'].forEach((section, index) => {\n const sectionItems = \"\".concat(section, \"Items\");\n items.add(section, m(FieldSet, {\n className: classList(\"Settings-\".concat(section), {\n 'FieldSet--col': section === 'account'\n }),\n label: app.translator.trans(\"core.forum.settings.\".concat(section, \"_heading\"))\n }, this[sectionItems]().toArray()), 100 - index * 10);\n });\n return items;\n }\n\n /**\n * Build an item list for the user's account settings.\n */\n accountItems() {\n const items = new ItemList();\n items.add('changePassword', m(Button, {\n className: \"Button\",\n onclick: () => app.modal.show(ChangePasswordModal)\n }, app.translator.trans('core.forum.settings.change_password_button')), 100);\n items.add('changeEmail', m(Button, {\n className: \"Button\",\n onclick: () => app.modal.show(ChangeEmailModal)\n }, app.translator.trans('core.forum.settings.change_email_button')), 90);\n return items;\n }\n\n /**\n * Build an item list for the user's notification settings.\n */\n notificationsItems() {\n const items = new ItemList();\n items.add('notificationGrid', m(NotificationGrid, {\n user: this.user\n }), 100);\n return items;\n }\n\n /**\n * Build an item list for the user's privacy settings.\n */\n privacyItems() {\n var _preferences;\n const items = new ItemList();\n items.add('discloseOnline', m(Switch, {\n state: (_preferences = this.user.preferences()) == null ? void 0 : _preferences.discloseOnline,\n onchange: value => {\n this.discloseOnlineLoading = true;\n this.user.savePreferences({\n discloseOnline: value\n }).then(() => {\n this.discloseOnlineLoading = false;\n m.redraw();\n });\n },\n loading: this.discloseOnlineLoading\n }, app.translator.trans('core.forum.settings.privacy_disclose_online_label')), 100);\n return items;\n }\n}\nflarum.reg.add('core', 'forum/components/SettingsPage', SettingsPage);","import app from '../../forum/app';\nimport Component from '../../common/Component';\nimport Checkbox from '../../common/components/Checkbox';\nimport ItemList from '../../common/utils/ItemList';\nimport Icon from '../../common/components/Icon';\n\n/**\n * The `NotificationGrid` component displays a table of notification types and\n * methods, allowing the user to toggle each combination.\n *\n * ### Attrs\n *\n * - `user`\n */\nexport default class NotificationGrid extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n\n /**\n * Information about the available notification methods.\n *\n * @type {({ name: string, icon: string, label: import('mithril').Children })[]}\n */\n this.methods = this.notificationMethods().toArray();\n\n /**\n * A map of which notification checkboxes are loading.\n *\n * @type {Record}\n */\n this.loading = {};\n\n /**\n * Information about the available notification types.\n *\n * @type {({ name: string, icon: string, label: import('mithril').Children })[]}\n */\n this.types = this.notificationTypes().toArray();\n }\n view() {\n const preferences = this.attrs.user.preferences();\n return m(\"table\", {\n className: \"NotificationGrid\"\n }, m(\"thead\", null, m(\"tr\", null, m(\"td\", null), this.methods.map(method => m(\"th\", {\n className: \"NotificationGrid-groupToggle\",\n onclick: this.toggleMethod.bind(this, method.name)\n }, m(Icon, {\n name: method.icon\n }), \" \", method.label)))), m(\"tbody\", null, this.types.map(type => m(\"tr\", null, m(\"td\", {\n className: \"NotificationGrid-groupToggle\",\n onclick: this.toggleType.bind(this, type.name)\n }, m(Icon, {\n name: type.icon\n }), \" \", type.label), this.methods.map(method => {\n const key = this.preferenceKey(type.name, method.name);\n return m(\"td\", {\n className: \"NotificationGrid-checkbox\"\n }, m(Checkbox, {\n state: !!preferences[key],\n loading: this.loading[key],\n disabled: !(key in preferences),\n onchange: this.toggle.bind(this, [key])\n }, m(\"span\", {\n className: \"sr-only\"\n }, app.translator.trans('core.forum.settings.notification_checkbox_a11y_label_template', {\n description: type.label,\n method: method.label\n }))));\n })))));\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {\n const i = parseInt($(this).index(), 10) + 1;\n $(this).parents('table').find('td:nth-child(' + i + ')').toggleClass('highlighted', e.type === 'mouseenter');\n });\n this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {\n $(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter');\n });\n }\n\n /**\n * Toggle the state of the given preferences, based on the value of the first\n * one.\n *\n * @param {string[]} keys\n */\n toggle(keys) {\n const user = this.attrs.user;\n const preferences = user.preferences();\n const enabled = !preferences[keys[0]];\n keys.forEach(key => {\n this.loading[key] = true;\n preferences[key] = enabled;\n });\n m.redraw();\n user.save({\n preferences\n }).then(() => {\n keys.forEach(key => this.loading[key] = false);\n m.redraw();\n });\n }\n\n /**\n * Toggle all notification types for the given method.\n *\n * @param {string} method\n */\n toggleMethod(method) {\n const keys = this.types.map(type => this.preferenceKey(type.name, method)).filter(key => key in this.attrs.user.preferences());\n this.toggle(keys);\n }\n\n /**\n * Toggle all notification methods for the given type.\n *\n * @param {string} type\n */\n toggleType(type) {\n const keys = this.methods.map(method => this.preferenceKey(type, method.name)).filter(key => key in this.attrs.user.preferences());\n this.toggle(keys);\n }\n\n /**\n * Get the name of the preference key for the given notification type-method\n * combination.\n *\n * @param {string} type\n * @param {string} method\n * @return {string}\n */\n preferenceKey(type, method) {\n return 'notify_' + type + '_' + method;\n }\n\n /**\n * Build an item list for the notification methods to display in the grid.\n *\n * Each notification method is an object which has the following properties:\n *\n * - `name` The name of the notification method.\n * - `icon` The icon to display in the column header.\n * - `label` The label to display in the column header.\n *\n * @return {ItemList<{ name: string, icon: string, label: import('mithril').Children }>}\n */\n notificationMethods() {\n const items = new ItemList();\n items.add('alert', {\n name: 'alert',\n icon: 'fas fa-bell',\n label: app.translator.trans('core.forum.settings.notify_by_web_heading')\n });\n items.add('email', {\n name: 'email',\n icon: 'far fa-envelope',\n label: app.translator.trans('core.forum.settings.notify_by_email_heading')\n });\n return items;\n }\n\n /**\n * Build an item list for the notification types to display in the grid.\n *\n * Each notification type is an object which has the following properties:\n *\n * - `name` The name of the notification type.\n * - `icon` The icon to display in the notification grid row.\n * - `label` The label to display in the notification grid row.\n *\n * @return {ItemList<{ name: string, icon: string, label: import('mithril').Children}>}\n */\n notificationTypes() {\n const items = new ItemList();\n items.add('discussionRenamed', {\n name: 'discussionRenamed',\n icon: 'fas fa-pencil-alt',\n label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label')\n });\n return items;\n }\n}\nflarum.reg.add('core', 'forum/components/NotificationGrid', NotificationGrid);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/SignUpModal.js b/framework/core/js/dist/forum/components/SignUpModal.js index ef23e0c69bf..968f241f01f 100644 --- a/framework/core/js/dist/forum/components/SignUpModal.js +++ b/framework/core/js/dist/forum/components/SignUpModal.js @@ -1,2 +1,223 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[395],{8686:(s,t,e)=>{e.r(t),e.d(t,{default:()=>h});var a=e(7905),r=e(6789),o=e(899),i=e(8312),n=e(6403),l=e(1552),d=e(4041),u=e(6458);class h extends o.Z{constructor(){super(...arguments),(0,a.Z)(this,"username",void 0),(0,a.Z)(this,"email",void 0),(0,a.Z)(this,"password",void 0)}oninit(s){super.oninit(s),this.username=(0,u.Z)(this.attrs.username||""),this.email=(0,u.Z)(this.attrs.email||""),this.password=(0,u.Z)(this.attrs.password||"")}className(){return"Modal--small SignUpModal"}title(){return r.Z.translator.trans("core.forum.sign_up.title")}content(){return[m("div",{className:"Modal-body"},this.body()),m("div",{className:"Modal-footer"},this.footer())]}isProvided(s){var t,e;return null!=(t=null==(e=this.attrs.provided)?void 0:e.includes(s))&&t}body(){return[!this.attrs.token&&m(n.Z,null),m("div",{className:"Form Form--centered"},this.fields().toArray())]}fields(){const s=new d.Z,t=(0,l.Z)(r.Z.translator.trans("core.forum.sign_up.username_placeholder")),e=(0,l.Z)(r.Z.translator.trans("core.forum.sign_up.email_placeholder")),a=(0,l.Z)(r.Z.translator.trans("core.forum.sign_up.password_placeholder"));return s.add("username",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"username",type:"text",placeholder:t,"aria-label":t,bidi:this.username,disabled:this.loading||this.isProvided("username")})),30),s.add("email",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"email",type:"email",placeholder:e,"aria-label":e,bidi:this.email,disabled:this.loading||this.isProvided("email")})),20),this.attrs.token||s.add("password",m("div",{className:"Form-group"},m("input",{className:"FormControl",name:"password",type:"password",autocomplete:"new-password",placeholder:a,"aria-label":a,bidi:this.password,disabled:this.loading})),10),s.add("submit",m("div",{className:"Form-group"},m(i.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},r.Z.translator.trans("core.forum.sign_up.submit_button"))),-10),s}footer(){return[m("p",{className:"SignUpModal-logIn"},r.Z.translator.trans("core.forum.sign_up.log_in_text",{a:m("a",{onclick:this.logIn.bind(this)})}))]}logIn(){const s={identification:this.email()||this.username()};r.Z.modal.show((()=>e.e(460).then(e.bind(e,5049))),s)}onready(){this.attrs.username&&!this.attrs.email?this.$("[name=email]").select():this.$("[name=username]").select()}onsubmit(s){s.preventDefault(),this.loading=!0;const t=this.submitData();r.Z.request({url:r.Z.forum.attribute("baseUrl")+"/register",method:"POST",body:t,errorHandler:this.onerror.bind(this)}).then((()=>window.location.reload()),this.loaded.bind(this))}submitData(){const s=this.attrs.token?{token:this.attrs.token}:{password:this.password()};return{username:this.username(),email:this.email(),...s}}}flarum.reg.add("core","forum/components/SignUpModal",h),flarum.reg.addChunkModule("460","5049","core","forum/components/LogInModal")},6403:(s,t,e)=>{e.d(t,{Z:()=>o});var a=e(2190),r=e(4041);class o extends a.Z{view(){return m("div",{className:"LogInButtons"},this.items().toArray())}items(){return new r.Z}}flarum.reg.add("core","forum/components/LogInButtons",o)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/SignUpModal"],{ + +/***/ "./src/forum/components/SignUpModal.tsx": +/*!**********************************************!*\ + !*** ./src/forum/components/SignUpModal.tsx ***! + \**********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ SignUpModal) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _LogInButtons__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./LogInButtons */ "./src/forum/components/LogInButtons.js"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts"); + + + + + + + + +class SignUpModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + /** + * The value of the username input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "username", void 0); + /** + * The value of the email input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "email", void 0); + /** + * The value of the password input. + */ + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "password", void 0); + } + oninit(vnode) { + super.oninit(vnode); + this.username = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.username || ''); + this.email = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.email || ''); + this.password = (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_7__["default"])(this.attrs.password || ''); + } + className() { + return 'Modal--small SignUpModal'; + } + title() { + return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.title'); + } + content() { + return [m("div", { + className: "Modal-body" + }, this.body()), m("div", { + className: "Modal-footer" + }, this.footer())]; + } + isProvided(field) { + var _this$attrs$provided$, _this$attrs$provided; + return (_this$attrs$provided$ = (_this$attrs$provided = this.attrs.provided) == null ? void 0 : _this$attrs$provided.includes(field)) != null ? _this$attrs$provided$ : false; + } + body() { + return [!this.attrs.token && m(_LogInButtons__WEBPACK_IMPORTED_MODULE_4__["default"], null), m("div", { + className: "Form Form--centered" + }, this.fields().toArray())]; + } + fields() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_6__["default"](); + const usernameLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.username_placeholder')); + const emailLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.email_placeholder')); + const passwordLabel = (0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_5__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.password_placeholder')); + items.add('username', m("div", { + className: "Form-group" + }, m("input", { + className: "FormControl", + name: "username", + type: "text", + placeholder: usernameLabel, + "aria-label": usernameLabel, + bidi: this.username, + disabled: this.loading || this.isProvided('username') + })), 30); + items.add('email', m("div", { + className: "Form-group" + }, m("input", { + className: "FormControl", + name: "email", + type: "email", + placeholder: emailLabel, + "aria-label": emailLabel, + bidi: this.email, + disabled: this.loading || this.isProvided('email') + })), 20); + if (!this.attrs.token) { + items.add('password', m("div", { + className: "Form-group" + }, m("input", { + className: "FormControl", + name: "password", + type: "password", + autocomplete: "new-password", + placeholder: passwordLabel, + "aria-label": passwordLabel, + bidi: this.password, + disabled: this.loading + })), 10); + } + items.add('submit', m("div", { + className: "Form-group" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary Button--block", + type: "submit", + loading: this.loading + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.submit_button'))), -10); + return items; + } + footer() { + return [m("p", { + className: "SignUpModal-logIn" + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.sign_up.log_in_text', { + a: m("a", { + onclick: this.logIn.bind(this) + }) + }))]; + } + + /** + * Open the log in modal, prefilling it with an email/username/password if + * the user has entered one. + */ + logIn() { + const attrs = { + identification: this.email() || this.username() + }; + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(() => __webpack_require__.e(/*! import() | forum/components/LogInModal */ "forum/components/LogInModal").then(__webpack_require__.bind(__webpack_require__, /*! ./LogInModal */ "./src/forum/components/LogInModal.tsx")), attrs); + } + onready() { + if (this.attrs.username && !this.attrs.email) { + this.$('[name=email]').select(); + } else { + this.$('[name=username]').select(); + } + } + onsubmit(e) { + e.preventDefault(); + this.loading = true; + const body = this.submitData(); + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].request({ + url: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('baseUrl') + '/register', + method: 'POST', + body, + errorHandler: this.onerror.bind(this) + }).then(() => window.location.reload(), this.loaded.bind(this)); + } + + /** + * Get the data that should be submitted in the sign-up request. + */ + submitData() { + const authData = this.attrs.token ? { + token: this.attrs.token + } : { + password: this.password() + }; + const data = { + username: this.username(), + email: this.email(), + ...authData + }; + return data; + } +} +flarum.reg.add('core', 'forum/components/SignUpModal', SignUpModal);flarum.reg.addChunkModule('forum/components/LogInModal', './src/forum/components/LogInModal.tsx', 'core', 'forum/components/LogInModal'); + +/***/ }), + +/***/ "./src/forum/components/LogInButtons.js": +/*!**********************************************!*\ + !*** ./src/forum/components/LogInButtons.js ***! + \**********************************************/ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ LogInButtons) +/* harmony export */ }); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); + + + +/** + * The `LogInButtons` component displays a collection of social login buttons. + */ +class LogInButtons extends _common_Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + view() { + return m("div", { + className: "LogInButtons" + }, this.items().toArray()); + } + + /** + * Build a list of LogInButton components. + * + * @return {ItemList} + */ + items() { + return new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_1__["default"](); + } +} +flarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons); + +/***/ }) + +}]); //# sourceMappingURL=SignUpModal.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/SignUpModal.js.map b/framework/core/js/dist/forum/components/SignUpModal.js.map index 3fa09028f5d..722a57b1d81 100644 --- a/framework/core/js/dist/forum/components/SignUpModal.js.map +++ b/framework/core/js/dist/forum/components/SignUpModal.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/SignUpModal.js","mappings":"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","sources":["webpack://@flarum/core/./src/forum/components/SignUpModal.tsx","webpack://@flarum/core/./src/forum/components/LogInButtons.js"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport LogInButtons from './LogInButtons';\nimport extractText from '../../common/utils/extractText';\nimport ItemList from '../../common/utils/ItemList';\nimport Stream from '../../common/utils/Stream';\nexport default class SignUpModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the username input.\n */\n _defineProperty(this, \"username\", void 0);\n /**\n * The value of the email input.\n */\n _defineProperty(this, \"email\", void 0);\n /**\n * The value of the password input.\n */\n _defineProperty(this, \"password\", void 0);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.username = Stream(this.attrs.username || '');\n this.email = Stream(this.attrs.email || '');\n this.password = Stream(this.attrs.password || '');\n }\n className() {\n return 'Modal--small SignUpModal';\n }\n title() {\n return app.translator.trans('core.forum.sign_up.title');\n }\n content() {\n return [m(\"div\", {\n className: \"Modal-body\"\n }, this.body()), m(\"div\", {\n className: \"Modal-footer\"\n }, this.footer())];\n }\n isProvided(field) {\n var _this$attrs$provided$, _this$attrs$provided;\n return (_this$attrs$provided$ = (_this$attrs$provided = this.attrs.provided) == null ? void 0 : _this$attrs$provided.includes(field)) != null ? _this$attrs$provided$ : false;\n }\n body() {\n return [!this.attrs.token && m(LogInButtons, null), m(\"div\", {\n className: \"Form Form--centered\"\n }, this.fields().toArray())];\n }\n fields() {\n const items = new ItemList();\n const usernameLabel = extractText(app.translator.trans('core.forum.sign_up.username_placeholder'));\n const emailLabel = extractText(app.translator.trans('core.forum.sign_up.email_placeholder'));\n const passwordLabel = extractText(app.translator.trans('core.forum.sign_up.password_placeholder'));\n items.add('username', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"username\",\n type: \"text\",\n placeholder: usernameLabel,\n \"aria-label\": usernameLabel,\n bidi: this.username,\n disabled: this.loading || this.isProvided('username')\n })), 30);\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"email\",\n type: \"email\",\n placeholder: emailLabel,\n \"aria-label\": emailLabel,\n bidi: this.email,\n disabled: this.loading || this.isProvided('email')\n })), 20);\n if (!this.attrs.token) {\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"password\",\n type: \"password\",\n autocomplete: \"new-password\",\n placeholder: passwordLabel,\n \"aria-label\": passwordLabel,\n bidi: this.password,\n disabled: this.loading\n })), 10);\n }\n items.add('submit', m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.sign_up.submit_button'))), -10);\n return items;\n }\n footer() {\n return [m(\"p\", {\n className: \"SignUpModal-logIn\"\n }, app.translator.trans('core.forum.sign_up.log_in_text', {\n a: m(\"a\", {\n onclick: this.logIn.bind(this)\n })\n }))];\n }\n\n /**\n * Open the log in modal, prefilling it with an email/username/password if\n * the user has entered one.\n */\n logIn() {\n const attrs = {\n identification: this.email() || this.username()\n };\n app.modal.show(() => import(/* webpackChunkName: 'forum/components/LogInModal', webpackMode: 'lazy-once' */ './LogInModal'), attrs);\n }\n onready() {\n if (this.attrs.username && !this.attrs.email) {\n this.$('[name=email]').select();\n } else {\n this.$('[name=username]').select();\n }\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n const body = this.submitData();\n app.request({\n url: app.forum.attribute('baseUrl') + '/register',\n method: 'POST',\n body,\n errorHandler: this.onerror.bind(this)\n }).then(() => window.location.reload(), this.loaded.bind(this));\n }\n\n /**\n * Get the data that should be submitted in the sign-up request.\n */\n submitData() {\n const authData = this.attrs.token ? {\n token: this.attrs.token\n } : {\n password: this.password()\n };\n const data = {\n username: this.username(),\n email: this.email(),\n ...authData\n };\n return data;\n }\n}\nflarum.reg.add('core', 'forum/components/SignUpModal', SignUpModal);flarum.reg.addChunkModule('460', '5049', 'core', 'forum/components/LogInModal');","import Component from '../../common/Component';\nimport ItemList from '../../common/utils/ItemList';\n\n/**\n * The `LogInButtons` component displays a collection of social login buttons.\n */\nexport default class LogInButtons extends Component {\n view() {\n return m(\"div\", {\n className: \"LogInButtons\"\n }, this.items().toArray());\n }\n\n /**\n * Build a list of LogInButton components.\n *\n * @return {ItemList}\n */\n items() {\n return new ItemList();\n }\n}\nflarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons);"],"names":["SignUpModal","constructor","super","arguments","this","oninit","vnode","username","attrs","email","password","className","title","content","m","body","footer","isProvided","field","_this$attrs$provided$","_this$attrs$provided","provided","includes","token","fields","toArray","items","usernameLabel","emailLabel","passwordLabel","add","name","type","placeholder","bidi","disabled","loading","autocomplete","a","onclick","logIn","bind","identification","onready","$","select","onsubmit","e","preventDefault","submitData","url","method","errorHandler","onerror","then","window","location","reload","loaded","authData","flarum","reg","addChunkModule","LogInButtons","view"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/SignUpModal.js","mappings":";;;;;;;;;;;;;;;;;;;;;AAAwE;AACtC;AACwB;AACN;AACV;AACe;AACN;AACJ;AAChC,0BAA0B,oEAAS;AAClD;AACA;AACA;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,IAAI,qFAAe;AACnB;AACA;AACA;AACA,oBAAoB,gEAAM;AAC1B,iBAAiB,gEAAM;AACvB,oBAAoB,gEAAM;AAC1B;AACA;AACA;AACA;AACA;AACA,WAAW,mEAAoB;AAC/B;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA,mCAAmC,qDAAY;AAC/C;AACA,KAAK;AACL;AACA;AACA,sBAAsB,8DAAQ;AAC9B,0BAA0B,qEAAW,CAAC,mEAAoB;AAC1D,uBAAuB,qEAAW,CAAC,mEAAoB;AACvD,0BAA0B,qEAAW,CAAC,mEAAoB;AAC1D;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,KAAK,IAAI,iEAAM;AACf;AACA;AACA;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA;AACA;AACA;AACA,KAAK,EAAE,mEAAoB;AAC3B;AACA;AACA,OAAO;AACP,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,6DAAc,OAAO,mNAAsG;AAC/H;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,0DAAW;AACf,WAAW,kEAAmB;AAC9B;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oEAAoE;;;;;;;;;;;;;;;;AC9JrB;AACI;;AAEnD;AACA;AACA;AACe,2BAA2B,yDAAS;AACnD;AACA;AACA;AACA,KAAK;AACL;;AAEA;AACA;AACA;AACA,cAAc;AACd;AACA;AACA,eAAe,8DAAQ;AACvB;AACA;AACA","sources":["webpack://@flarum/core/./src/forum/components/SignUpModal.tsx","webpack://@flarum/core/./src/forum/components/LogInButtons.js"],"sourcesContent":["import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport LogInButtons from './LogInButtons';\nimport extractText from '../../common/utils/extractText';\nimport ItemList from '../../common/utils/ItemList';\nimport Stream from '../../common/utils/Stream';\nexport default class SignUpModal extends FormModal {\n constructor() {\n super(...arguments);\n /**\n * The value of the username input.\n */\n _defineProperty(this, \"username\", void 0);\n /**\n * The value of the email input.\n */\n _defineProperty(this, \"email\", void 0);\n /**\n * The value of the password input.\n */\n _defineProperty(this, \"password\", void 0);\n }\n oninit(vnode) {\n super.oninit(vnode);\n this.username = Stream(this.attrs.username || '');\n this.email = Stream(this.attrs.email || '');\n this.password = Stream(this.attrs.password || '');\n }\n className() {\n return 'Modal--small SignUpModal';\n }\n title() {\n return app.translator.trans('core.forum.sign_up.title');\n }\n content() {\n return [m(\"div\", {\n className: \"Modal-body\"\n }, this.body()), m(\"div\", {\n className: \"Modal-footer\"\n }, this.footer())];\n }\n isProvided(field) {\n var _this$attrs$provided$, _this$attrs$provided;\n return (_this$attrs$provided$ = (_this$attrs$provided = this.attrs.provided) == null ? void 0 : _this$attrs$provided.includes(field)) != null ? _this$attrs$provided$ : false;\n }\n body() {\n return [!this.attrs.token && m(LogInButtons, null), m(\"div\", {\n className: \"Form Form--centered\"\n }, this.fields().toArray())];\n }\n fields() {\n const items = new ItemList();\n const usernameLabel = extractText(app.translator.trans('core.forum.sign_up.username_placeholder'));\n const emailLabel = extractText(app.translator.trans('core.forum.sign_up.email_placeholder'));\n const passwordLabel = extractText(app.translator.trans('core.forum.sign_up.password_placeholder'));\n items.add('username', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"username\",\n type: \"text\",\n placeholder: usernameLabel,\n \"aria-label\": usernameLabel,\n bidi: this.username,\n disabled: this.loading || this.isProvided('username')\n })), 30);\n items.add('email', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"email\",\n type: \"email\",\n placeholder: emailLabel,\n \"aria-label\": emailLabel,\n bidi: this.email,\n disabled: this.loading || this.isProvided('email')\n })), 20);\n if (!this.attrs.token) {\n items.add('password', m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n className: \"FormControl\",\n name: \"password\",\n type: \"password\",\n autocomplete: \"new-password\",\n placeholder: passwordLabel,\n \"aria-label\": passwordLabel,\n bidi: this.password,\n disabled: this.loading\n })), 10);\n }\n items.add('submit', m(\"div\", {\n className: \"Form-group\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.sign_up.submit_button'))), -10);\n return items;\n }\n footer() {\n return [m(\"p\", {\n className: \"SignUpModal-logIn\"\n }, app.translator.trans('core.forum.sign_up.log_in_text', {\n a: m(\"a\", {\n onclick: this.logIn.bind(this)\n })\n }))];\n }\n\n /**\n * Open the log in modal, prefilling it with an email/username/password if\n * the user has entered one.\n */\n logIn() {\n const attrs = {\n identification: this.email() || this.username()\n };\n app.modal.show(() => import(/* webpackChunkName: 'forum/components/LogInModal', webpackMode: 'lazy-once' */ './LogInModal'), attrs);\n }\n onready() {\n if (this.attrs.username && !this.attrs.email) {\n this.$('[name=email]').select();\n } else {\n this.$('[name=username]').select();\n }\n }\n onsubmit(e) {\n e.preventDefault();\n this.loading = true;\n const body = this.submitData();\n app.request({\n url: app.forum.attribute('baseUrl') + '/register',\n method: 'POST',\n body,\n errorHandler: this.onerror.bind(this)\n }).then(() => window.location.reload(), this.loaded.bind(this));\n }\n\n /**\n * Get the data that should be submitted in the sign-up request.\n */\n submitData() {\n const authData = this.attrs.token ? {\n token: this.attrs.token\n } : {\n password: this.password()\n };\n const data = {\n username: this.username(),\n email: this.email(),\n ...authData\n };\n return data;\n }\n}\nflarum.reg.add('core', 'forum/components/SignUpModal', SignUpModal);flarum.reg.addChunkModule('forum/components/LogInModal', './src/forum/components/LogInModal.tsx', 'core', 'forum/components/LogInModal');","import Component from '../../common/Component';\nimport ItemList from '../../common/utils/ItemList';\n\n/**\n * The `LogInButtons` component displays a collection of social login buttons.\n */\nexport default class LogInButtons extends Component {\n view() {\n return m(\"div\", {\n className: \"LogInButtons\"\n }, this.items().toArray());\n }\n\n /**\n * Build a list of LogInButton components.\n *\n * @return {ItemList}\n */\n items() {\n return new ItemList();\n }\n}\nflarum.reg.add('core', 'forum/components/LogInButtons', LogInButtons);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/UserSecurityPage.js b/framework/core/js/dist/forum/components/UserSecurityPage.js index 30f8f551378..aa5e034a371 100644 --- a/framework/core/js/dist/forum/components/UserSecurityPage.js +++ b/framework/core/js/dist/forum/components/UserSecurityPage.js @@ -1,2 +1,561 @@ -"use strict";(self.webpackChunkflarum_core=self.webpackChunkflarum_core||[]).push([[505],{2828:(e,s,t)=>{t.r(s),t.d(s,{default:()=>L});var o=t(7905),a=t(6789),r=t(3390),n=t(4041),i=t(8034),l=t(1268),c=t(1552),u=t(2190),d=t(8312),h=t(9841),k=t(7465);class Z extends u.Z{view(e){return m("div",{className:"LabelValue"},m("div",{className:"LabelValue-label"},k.Z.translator.trans("core.lib.data_segment.label",{label:this.attrs.label})),m("div",{className:"LabelValue-value"},this.attrs.value))}}flarum.reg.add("core","common/components/LabelValue",Z);var g=t(3344),f=t(6439),v=t(9133);class T extends u.Z{constructor(){super(...arguments),(0,o.Z)(this,"loading",{}),(0,o.Z)(this,"showingTokens",{})}view(e){return m("div",{className:"AccessTokensList"},this.attrs.tokens.length?this.attrs.tokens.map(this.tokenView.bind(this)):m("div",{className:"AccessTokensList--empty"},a.Z.translator.trans("core.forum.security.empty_text")))}tokenView(e){return m("div",{className:(0,g.Z)("AccessTokensList-item",{"AccessTokensList-item--active":e.isCurrent()}),key:e.id()},this.tokenViewItems(e).toArray())}tokenViewItems(e){const s=new n.Z;return s.add("icon",m("div",{className:"AccessTokensList-item-icon"},m(v.Z,{name:this.attrs.icon||"fas fa-key"})),50),s.add("info",m("div",{className:"AccessTokensList-item-info"},this.tokenInfoItems(e).toArray()),40),s.add("actions",m("div",{className:"AccessTokensList-item-actions"},this.tokenActionItems(e).toArray()),30),s}tokenInfoItems(e){const s=new n.Z;return"session"===this.attrs.type?s.add("title",m("div",{className:"AccessTokensList-item-title"},m("span",{className:"AccessTokensList-item-title-main"},e.device()),e.isCurrent()&&[" — ",m("span",{className:"AccessTokensList-item-title-sub"},a.Z.translator.trans("core.forum.security.current_active_session"))])):s.add("title",m("div",{className:"AccessTokensList-item-title"},m("span",{className:"AccessTokensList-item-title-main"},this.generateTokenTitle(e)))),s.add("createdAt",m("div",{className:"AccessTokensList-item-createdAt"},m(Z,{label:a.Z.translator.trans("core.forum.security.created"),value:(0,h.Z)(e.createdAt())}))),s.add("lastActivityAt",m("div",{className:"AccessTokensList-item-lastActivityAt"},m(Z,{label:a.Z.translator.trans("core.forum.security.last_activity"),value:e.lastActivityAt()?m("[",null,(0,h.Z)(e.lastActivityAt()),e.lastIpAddress()&&" — ".concat(e.lastIpAddress()),"developer_token"===this.attrs.type&&e.device()&&m("[",null," ","— ",m("span",{className:"AccessTokensList-item-title-sub"},e.device()))):a.Z.translator.trans("core.forum.security.never")}))),s}tokenActionItems(e){const s=new n.Z,t={session:"terminate_session",developer_token:"revoke_access_token"}[this.attrs.type];if("developer_token"===this.attrs.type){const t=!this.showingTokens[e.id()],o=t?"show_access_token":"hide_access_token";s.add("toggleDisplay",m(d.Z,{className:"Button Button--inverted",icon:t?"fas fa-eye":"fas fa-eye-slash",onclick:()=>{this.showingTokens[e.id()]=t,m.redraw()}},a.Z.translator.trans("core.forum.security.".concat(o))))}let o=m(d.Z,{className:"Button Button--danger",disabled:e.isCurrent(),loading:!!this.loading[e.id()],onclick:()=>this.revoke(e)},a.Z.translator.trans("core.forum.security.".concat(t)));return e.isCurrent()&&(o=m(f.Z,{text:a.Z.translator.trans("core.forum.security.cannot_terminate_current_session")},m("div",{tabindex:"0"},o))),s.add("revoke",o),s}async revoke(e){var s,t;if(!confirm((0,c.Z)(a.Z.translator.trans("core.forum.security.revoke_access_token_confirmation"))))return;this.loading[e.id()]=!0,await e.delete(),this.loading[e.id()]=!1,null==(s=(t=this.attrs).ondelete)||s.call(t,e);const o="session"===this.attrs.type?"session_terminated":"token_revoked";a.Z.alerts.show({type:"success"},a.Z.translator.trans("core.forum.security.".concat(o),{count:1})),m.redraw()}generateTokenTitle(e){const s=e.title()||a.Z.translator.trans("core.forum.security.token_title_placeholder"),t=this.tokenValueDisplay(e);return a.Z.translator.trans("core.forum.security.token_item_title",{title:s,token:t})}tokenValueDisplay(e){const s=Array(12).fill("*").join(""),t=this.showingTokens[e.id()]?e.token():s;return m("code",{className:"AccessTokensList-item-token"},t)}}flarum.reg.add("core","forum/components/AccessTokensList",T);var y=t(5226),p=t(899),_=t(6458),b=t(6352);class A extends p.Z{constructor(){super(...arguments),(0,o.Z)(this,"titleInput",(0,_.Z)(""))}className(){return"Modal--small NewAccessTokenModal"}title(){return a.Z.translator.trans("core.forum.security.new_access_token_modal.title")}content(){const e=a.Z.translator.trans("core.forum.security.new_access_token_modal.title_placeholder");return m("div",{className:"Modal-body"},m(b.Z,{className:"Form--centered"},m("div",{className:"Form-group"},m("input",{type:"text",className:"FormControl",bidi:this.titleInput,placeholder:e,"aria-label":e})),m("div",{className:"Form-group Form-controls"},m(d.Z,{className:"Button Button--primary Button--block",type:"submit",loading:this.loading},a.Z.translator.trans("core.forum.security.new_access_token_modal.submit_button")))))}submitData(){return{title:this.titleInput()}}onsubmit(e){super.onsubmit(e),e.preventDefault(),this.loading=!0,a.Z.store.createRecord("access-tokens").save(this.submitData()).then((e=>{this.attrs.onsuccess(e),a.Z.modal.close()})).finally(this.loaded.bind(this))}}flarum.reg.add("core","forum/components/NewAccessTokenModal",A);class w{constructor(){(0,o.Z)(this,"tokens",null),(0,o.Z)(this,"loadingTerminateSessions",!1),(0,o.Z)(this,"loadingGlobalLogout",!1)}hasLoadedTokens(){return null!==this.tokens}getTokens(){return this.tokens}setTokens(e){this.tokens=e}pushToken(e){var s;null==(s=this.tokens)||s.push(e)}removeToken(e){this.tokens=this.tokens.filter((s=>s!==e))}getSessionTokens(){var e;return(null==(e=this.tokens)?void 0:e.filter((e=>e.isSessionToken())).sort(((e,s)=>s.isCurrent()?1:-1)))||[]}getDeveloperTokens(){var e;return(null==(e=this.tokens)?void 0:e.filter((e=>!e.isSessionToken())))||null}getOtherSessionTokens(){var e;return(null==(e=this.tokens)?void 0:e.filter((e=>e.isSessionToken()&&!e.isCurrent())))||[]}hasOtherActiveSessions(){return(this.getOtherSessionTokens()||[]).length>0}removeOtherSessionTokens(){this.tokens=this.tokens.filter((e=>!e.isSessionToken()||e.isCurrent()))}}flarum.reg.add("core","forum/states/UserSecurityPageState",w);class L extends r.Z{constructor(){super(...arguments),(0,o.Z)(this,"state",new w)}oninit(e){var s;super.oninit(e);const t=m.route.param("username");t===(null==(s=a.Z.session.user)?void 0:s.slug())||a.Z.forum.attribute("canModerateAccessTokens")||m.route.set("/"),this.loadUser(t),a.Z.setTitle((0,c.Z)(a.Z.translator.trans("core.forum.security.title"))),this.loadTokens()}content(){return m("div",{className:"UserSecurityPage"},m("ul",null,(0,l.Z)(this.settingsItems().toArray())))}settingsItems(){var e;const s=new n.Z;return a.Z.forum.attribute("canCreateAccessToken")||a.Z.forum.attribute("canModerateAccessTokens")||this.state.hasLoadedTokens()&&null!=(e=this.state.getDeveloperTokens())&&e.length?s.add("developerTokens",m(i.Z,{className:"UserSecurityPage-developerTokens",label:a.Z.translator.trans("core.forum.security.developer_tokens_heading")},this.developerTokensItems().toArray())):this.state.hasLoadedTokens()||s.add("developerTokens",m(y.Z,null)),s.add("sessions",m(i.Z,{className:"UserSecurityPage-sessions",label:a.Z.translator.trans("core.forum.security.sessions_heading")},this.sessionsItems().toArray())),this.user.id()===a.Z.session.user.id()&&s.add("globalLogout",m(i.Z,{className:"FieldSet--col UserSecurityPage-globalLogout",label:a.Z.translator.trans("core.forum.security.global_logout.heading"),description:a.Z.translator.trans("core.forum.security.global_logout.help_text")},m(d.Z,{className:"Button",icon:"fas fa-sign-out-alt",onclick:this.globalLogout.bind(this),loading:this.state.loadingGlobalLogout,disabled:this.state.loadingTerminateSessions},a.Z.translator.trans("core.forum.security.global_logout.log_out_button")))),s}developerTokensItems(){const e=new n.Z;return e.add("accessTokenList",this.state.hasLoadedTokens()?m(T,{type:"developer_token",ondelete:e=>{this.state.removeToken(e),m.redraw()},tokens:this.state.getDeveloperTokens(),icon:"fas fa-key",hideTokens:!1}):m(y.Z,null)),this.user.id()===a.Z.session.user.id()&&e.add("newAccessToken",m("div",{className:"UserSecurityPage-controls"},m(d.Z,{className:"Button",disabled:!a.Z.forum.attribute("canCreateAccessToken"),onclick:()=>a.Z.modal.show(A,{onsuccess:e=>{this.state.pushToken(e),m.redraw()}})},a.Z.translator.trans("core.forum.security.new_access_token_button")))),e}sessionsItems(){const e=new n.Z;if(e.add("sessionsList",this.state.hasLoadedTokens()?m(T,{type:"session",ondelete:e=>{this.state.removeToken(e),m.redraw()},tokens:this.state.getSessionTokens(),icon:"fas fa-laptop",hideTokens:!0}):m(y.Z,null)),this.user.id()===a.Z.session.user.id()){const s=!this.state.hasOtherActiveSessions();let t=m(d.Z,{className:"Button",onclick:this.terminateAllOtherSessions.bind(this),loading:this.state.loadingTerminateSessions,disabled:this.state.loadingGlobalLogout||s},a.Z.translator.trans("core.forum.security.terminate_all_other_sessions"));s&&(t=m(f.Z,{text:a.Z.translator.trans("core.forum.security.cannot_terminate_current_session")},m("span",{tabindex:"0"},t))),e.add("terminateAllOtherSessions",m("div",{className:"UserSecurityPage-controls"},t))}return e}loadTokens(){return a.Z.store.find("access-tokens",{filter:{user:this.user.id()}}).then((e=>{this.state.setTokens(e),m.redraw()}))}terminateAllOtherSessions(){if(confirm((0,c.Z)(a.Z.translator.trans("core.forum.security.terminate_all_other_sessions_confirmation"))))return this.state.loadingTerminateSessions=!0,a.Z.request({method:"DELETE",url:a.Z.forum.attribute("apiUrl")+"/sessions"}).then((()=>{const e=this.state.getOtherSessionTokens().length;this.state.removeOtherSessionTokens(),a.Z.alerts.show({type:"success"},a.Z.translator.trans("core.forum.security.session_terminated",{count:e}))})).catch((()=>{a.Z.alerts.show({type:"error"},a.Z.translator.trans("core.forum.security.session_termination_failed"))})).finally((()=>{this.state.loadingTerminateSessions=!1,m.redraw()}))}globalLogout(){return this.state.loadingGlobalLogout=!0,a.Z.request({method:"POST",url:a.Z.forum.attribute("baseUrl")+"/global-logout"}).then((()=>window.location.reload())).finally((()=>{this.state.loadingGlobalLogout=!1,m.redraw()}))}}flarum.reg.add("core","forum/components/UserSecurityPage",L)}}]); +"use strict"; +(self["webpackChunkflarum_core"] = self["webpackChunkflarum_core"] || []).push([["forum/components/UserSecurityPage"],{ + +/***/ "./src/common/components/LabelValue.tsx": +/*!**********************************************!*\ + !*** ./src/common/components/LabelValue.tsx ***! + \**********************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ LabelValue) +/* harmony export */ }); +/* harmony import */ var _Component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Component */ "./src/common/Component.ts"); +/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../app */ "./src/common/app.ts"); + + +/** + * A generic component for displaying a label and value inline. + * Created to avoid reinventing the wheel. + * + * `label: value` + */ +class LabelValue extends _Component__WEBPACK_IMPORTED_MODULE_0__["default"] { + view(vnode) { + return m("div", { + className: "LabelValue" + }, m("div", { + className: "LabelValue-label" + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.lib.data_segment.label', { + label: this.attrs.label + })), m("div", { + className: "LabelValue-value" + }, this.attrs.value)); + } +} +flarum.reg.add('core', 'common/components/LabelValue', LabelValue); + +/***/ }), + +/***/ "./src/forum/components/AccessTokensList.tsx": +/*!***************************************************!*\ + !*** ./src/forum/components/AccessTokensList.tsx ***! + \***************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ AccessTokensList) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../app */ "./src/forum/app.ts"); +/* harmony import */ var _common_Component__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/Component */ "./src/common/Component.ts"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_helpers_humanTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/helpers/humanTime */ "./src/common/helpers/humanTime.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_components_LabelValue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/components/LabelValue */ "./src/common/components/LabelValue.tsx"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _common_utils_classList__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../../common/utils/classList */ "./src/common/utils/classList.ts"); +/* harmony import */ var _common_components_Tooltip__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../../common/components/Tooltip */ "./src/common/components/Tooltip.tsx"); +/* harmony import */ var _common_components_Icon__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../../common/components/Icon */ "./src/common/components/Icon.tsx"); + + + + + + + + + + + +class AccessTokensList extends _common_Component__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "loading", {}); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "showingTokens", {}); + } + view(vnode) { + return m("div", { + className: "AccessTokensList" + }, this.attrs.tokens.length ? this.attrs.tokens.map(this.tokenView.bind(this)) : m("div", { + className: "AccessTokensList--empty" + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.empty_text'))); + } + tokenView(token) { + return m("div", { + className: (0,_common_utils_classList__WEBPACK_IMPORTED_MODULE_8__["default"])('AccessTokensList-item', { + 'AccessTokensList-item--active': token.isCurrent() + }), + key: token.id() + }, this.tokenViewItems(token).toArray()); + } + tokenViewItems(token) { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"](); + items.add('icon', m("div", { + className: "AccessTokensList-item-icon" + }, m(_common_components_Icon__WEBPACK_IMPORTED_MODULE_10__["default"], { + name: this.attrs.icon || 'fas fa-key' + })), 50); + items.add('info', m("div", { + className: "AccessTokensList-item-info" + }, this.tokenInfoItems(token).toArray()), 40); + items.add('actions', m("div", { + className: "AccessTokensList-item-actions" + }, this.tokenActionItems(token).toArray()), 30); + return items; + } + tokenInfoItems(token) { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"](); + if (this.attrs.type === 'session') { + items.add('title', m("div", { + className: "AccessTokensList-item-title" + }, m("span", { + className: "AccessTokensList-item-title-main" + }, token.device()), token.isCurrent() && [' — ', m("span", { + className: "AccessTokensList-item-title-sub" + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.current_active_session'))])); + } else { + items.add('title', m("div", { + className: "AccessTokensList-item-title" + }, m("span", { + className: "AccessTokensList-item-title-main" + }, this.generateTokenTitle(token)))); + } + items.add('createdAt', m("div", { + className: "AccessTokensList-item-createdAt" + }, m(_common_components_LabelValue__WEBPACK_IMPORTED_MODULE_6__["default"], { + label: _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.created'), + value: (0,_common_helpers_humanTime__WEBPACK_IMPORTED_MODULE_4__["default"])(token.createdAt()) + }))); + items.add('lastActivityAt', m("div", { + className: "AccessTokensList-item-lastActivityAt" + }, m(_common_components_LabelValue__WEBPACK_IMPORTED_MODULE_6__["default"], { + label: _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.last_activity'), + value: token.lastActivityAt() ? m('[', null, (0,_common_helpers_humanTime__WEBPACK_IMPORTED_MODULE_4__["default"])(token.lastActivityAt()), token.lastIpAddress() && " \u2014 ".concat(token.lastIpAddress()), this.attrs.type === 'developer_token' && token.device() && m('[', null, ' ', "\u2014 ", m("span", { + className: "AccessTokensList-item-title-sub" + }, token.device()))) : _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.never') + }))); + return items; + } + tokenActionItems(token) { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_5__["default"](); + const deleteKey = { + session: 'terminate_session', + developer_token: 'revoke_access_token' + }[this.attrs.type]; + if (this.attrs.type === 'developer_token') { + const isHidden = !this.showingTokens[token.id()]; + const displayKey = isHidden ? 'show_access_token' : 'hide_access_token'; + items.add('toggleDisplay', m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--inverted", + icon: isHidden ? 'fas fa-eye' : 'fas fa-eye-slash', + onclick: () => { + this.showingTokens[token.id()] = isHidden; + m.redraw(); + } + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans("core.forum.security.".concat(displayKey)))); + } + let revokeButton = m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--danger", + disabled: token.isCurrent(), + loading: !!this.loading[token.id()], + onclick: () => this.revoke(token) + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans("core.forum.security.".concat(deleteKey))); + if (token.isCurrent()) { + revokeButton = m(_common_components_Tooltip__WEBPACK_IMPORTED_MODULE_9__["default"], { + text: _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.cannot_terminate_current_session') + }, m("div", { + tabindex: "0" + }, revokeButton)); + } + items.add('revoke', revokeButton); + return items; + } + async revoke(token) { + var _this$attrs$ondelete, _this$attrs; + if (!confirm((0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_7__["default"])(_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.revoke_access_token_confirmation')))) return; + this.loading[token.id()] = true; + await token.delete(); + this.loading[token.id()] = false; + (_this$attrs$ondelete = (_this$attrs = this.attrs).ondelete) == null ? void 0 : _this$attrs$ondelete.call(_this$attrs, token); + const key = this.attrs.type === 'session' ? 'session_terminated' : 'token_revoked'; + _app__WEBPACK_IMPORTED_MODULE_1__["default"].alerts.show({ + type: 'success' + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans("core.forum.security.".concat(key), { + count: 1 + })); + m.redraw(); + } + generateTokenTitle(token) { + const name = token.title() || _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.token_title_placeholder'); + const value = this.tokenValueDisplay(token); + return _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.token_item_title', { + title: name, + token: value + }); + } + tokenValueDisplay(token) { + const obfuscatedName = Array(12).fill('*').join(''); + const value = this.showingTokens[token.id()] ? token.token() : obfuscatedName; + return m("code", { + className: "AccessTokensList-item-token" + }, value); + } +} +flarum.reg.add('core', 'forum/components/AccessTokensList', AccessTokensList); + +/***/ }), + +/***/ "./src/forum/components/NewAccessTokenModal.tsx": +/*!******************************************************!*\ + !*** ./src/forum/components/NewAccessTokenModal.tsx ***! + \******************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ NewAccessTokenModal) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../app */ "./src/forum/app.ts"); +/* harmony import */ var _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../common/components/FormModal */ "./src/common/components/FormModal.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _common_utils_Stream__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/utils/Stream */ "./src/common/utils/Stream.ts"); +/* harmony import */ var _common_components_Form__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/components/Form */ "./src/common/components/Form.tsx"); + + + + + + +class NewAccessTokenModal extends _common_components_FormModal__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "titleInput", (0,_common_utils_Stream__WEBPACK_IMPORTED_MODULE_4__["default"])('')); + } + className() { + return 'Modal--small NewAccessTokenModal'; + } + title() { + return _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.new_access_token_modal.title'); + } + content() { + const titleLabel = _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.new_access_token_modal.title_placeholder'); + return m("div", { + className: "Modal-body" + }, m(_common_components_Form__WEBPACK_IMPORTED_MODULE_5__["default"], { + className: "Form--centered" + }, m("div", { + className: "Form-group" + }, m("input", { + type: "text", + className: "FormControl", + bidi: this.titleInput, + placeholder: titleLabel, + "aria-label": titleLabel + })), m("div", { + className: "Form-group Form-controls" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_3__["default"], { + className: "Button Button--primary Button--block", + type: "submit", + loading: this.loading + }, _app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.new_access_token_modal.submit_button'))))); + } + submitData() { + return { + title: this.titleInput() + }; + } + onsubmit(e) { + super.onsubmit(e); + e.preventDefault(); + this.loading = true; + _app__WEBPACK_IMPORTED_MODULE_1__["default"].store.createRecord('access-tokens').save(this.submitData()).then(token => { + this.attrs.onsuccess(token); + _app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.close(); + }).finally(this.loaded.bind(this)); + } +} +flarum.reg.add('core', 'forum/components/NewAccessTokenModal', NewAccessTokenModal); + +/***/ }), + +/***/ "./src/forum/components/UserSecurityPage.tsx": +/*!***************************************************!*\ + !*** ./src/forum/components/UserSecurityPage.tsx ***! + \***************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ UserSecurityPage) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); +/* harmony import */ var _forum_app__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../forum/app */ "./src/forum/app.ts"); +/* harmony import */ var _UserPage__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./UserPage */ "./src/forum/components/UserPage.tsx"); +/* harmony import */ var _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../common/utils/ItemList */ "./src/common/utils/ItemList.ts"); +/* harmony import */ var _common_components_FieldSet__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../common/components/FieldSet */ "./src/common/components/FieldSet.tsx"); +/* harmony import */ var _common_helpers_listItems__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../../common/helpers/listItems */ "./src/common/helpers/listItems.tsx"); +/* harmony import */ var _common_utils_extractText__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../../common/utils/extractText */ "./src/common/utils/extractText.ts"); +/* harmony import */ var _AccessTokensList__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./AccessTokensList */ "./src/forum/components/AccessTokensList.tsx"); +/* harmony import */ var _common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../../common/components/LoadingIndicator */ "./src/common/components/LoadingIndicator.tsx"); +/* harmony import */ var _common_components_Button__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../../common/components/Button */ "./src/common/components/Button.tsx"); +/* harmony import */ var _NewAccessTokenModal__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./NewAccessTokenModal */ "./src/forum/components/NewAccessTokenModal.tsx"); +/* harmony import */ var _common_components_Tooltip__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../../common/components/Tooltip */ "./src/common/components/Tooltip.tsx"); +/* harmony import */ var _states_UserSecurityPageState__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../states/UserSecurityPageState */ "./src/forum/states/UserSecurityPageState.ts"); + + + + + + + + + + + + + + +/** + * The `UserSecurityPage` component displays the user's security control panel, in + * the context of their user profile. + */ +class UserSecurityPage extends _UserPage__WEBPACK_IMPORTED_MODULE_2__["default"] { + constructor() { + super(...arguments); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "state", new _states_UserSecurityPageState__WEBPACK_IMPORTED_MODULE_12__["default"]()); + } + oninit(vnode) { + var _app$session$user; + super.oninit(vnode); + const routeUsername = m.route.param('username'); + if (routeUsername !== ((_app$session$user = _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user) == null ? void 0 : _app$session$user.slug()) && !_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('canModerateAccessTokens')) { + m.route.set('/'); + } + this.loadUser(routeUsername); + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].setTitle((0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.title'))); + this.loadTokens(); + } + content() { + return m("div", { + className: "UserSecurityPage" + }, m("ul", null, (0,_common_helpers_listItems__WEBPACK_IMPORTED_MODULE_5__["default"])(this.settingsItems().toArray()))); + } + + /** + * Build an item list for the user's settings controls. + */ + settingsItems() { + var _this$state$getDevelo; + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + if (_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('canCreateAccessToken') || _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('canModerateAccessTokens') || this.state.hasLoadedTokens() && (_this$state$getDevelo = this.state.getDeveloperTokens()) != null && _this$state$getDevelo.length) { + items.add('developerTokens', m(_common_components_FieldSet__WEBPACK_IMPORTED_MODULE_4__["default"], { + className: "UserSecurityPage-developerTokens", + label: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans("core.forum.security.developer_tokens_heading") + }, this.developerTokensItems().toArray())); + } else if (!this.state.hasLoadedTokens()) { + items.add('developerTokens', m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_8__["default"], null)); + } + items.add('sessions', m(_common_components_FieldSet__WEBPACK_IMPORTED_MODULE_4__["default"], { + className: "UserSecurityPage-sessions", + label: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans("core.forum.security.sessions_heading") + }, this.sessionsItems().toArray())); + if (this.user.id() === _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user.id()) { + items.add('globalLogout', m(_common_components_FieldSet__WEBPACK_IMPORTED_MODULE_4__["default"], { + className: "FieldSet--col UserSecurityPage-globalLogout", + label: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.global_logout.heading'), + description: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.global_logout.help_text') + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_9__["default"], { + className: "Button", + icon: "fas fa-sign-out-alt", + onclick: this.globalLogout.bind(this), + loading: this.state.loadingGlobalLogout, + disabled: this.state.loadingTerminateSessions + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.global_logout.log_out_button')))); + } + return items; + } + + /** + * Build an item list for the user's access accessToken settings. + */ + developerTokensItems() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + items.add('accessTokenList', !this.state.hasLoadedTokens() ? m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_8__["default"], null) : m(_AccessTokensList__WEBPACK_IMPORTED_MODULE_7__["default"], { + type: "developer_token", + ondelete: token => { + this.state.removeToken(token); + m.redraw(); + }, + tokens: this.state.getDeveloperTokens(), + icon: "fas fa-key", + hideTokens: false + })); + if (this.user.id() === _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user.id()) { + items.add('newAccessToken', m("div", { + className: "UserSecurityPage-controls" + }, m(_common_components_Button__WEBPACK_IMPORTED_MODULE_9__["default"], { + className: "Button", + disabled: !_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('canCreateAccessToken'), + onclick: () => _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].modal.show(_NewAccessTokenModal__WEBPACK_IMPORTED_MODULE_10__["default"], { + onsuccess: token => { + this.state.pushToken(token); + m.redraw(); + } + }) + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.new_access_token_button')))); + } + return items; + } + + /** + * Build an item list for the user's access accessToken settings. + */ + sessionsItems() { + const items = new _common_utils_ItemList__WEBPACK_IMPORTED_MODULE_3__["default"](); + items.add('sessionsList', !this.state.hasLoadedTokens() ? m(_common_components_LoadingIndicator__WEBPACK_IMPORTED_MODULE_8__["default"], null) : m(_AccessTokensList__WEBPACK_IMPORTED_MODULE_7__["default"], { + type: "session", + ondelete: token => { + this.state.removeToken(token); + m.redraw(); + }, + tokens: this.state.getSessionTokens(), + icon: "fas fa-laptop", + hideTokens: true + })); + if (this.user.id() === _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].session.user.id()) { + const isDisabled = !this.state.hasOtherActiveSessions(); + let terminateAllOthersButton = m(_common_components_Button__WEBPACK_IMPORTED_MODULE_9__["default"], { + className: "Button", + onclick: this.terminateAllOtherSessions.bind(this), + loading: this.state.loadingTerminateSessions, + disabled: this.state.loadingGlobalLogout || isDisabled + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.terminate_all_other_sessions')); + if (isDisabled) { + terminateAllOthersButton = m(_common_components_Tooltip__WEBPACK_IMPORTED_MODULE_11__["default"], { + text: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.cannot_terminate_current_session') + }, m("span", { + tabindex: "0" + }, terminateAllOthersButton)); + } + items.add('terminateAllOtherSessions', m("div", { + className: "UserSecurityPage-controls" + }, terminateAllOthersButton)); + } + return items; + } + loadTokens() { + return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].store.find('access-tokens', { + filter: { + user: this.user.id() + } + }).then(tokens => { + this.state.setTokens(tokens); + m.redraw(); + }); + } + terminateAllOtherSessions() { + if (!confirm((0,_common_utils_extractText__WEBPACK_IMPORTED_MODULE_6__["default"])(_forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.terminate_all_other_sessions_confirmation')))) return; + this.state.loadingTerminateSessions = true; + return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].request({ + method: 'DELETE', + url: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('apiUrl') + '/sessions' + }).then(() => { + // Count terminated sessions first. + const count = this.state.getOtherSessionTokens().length; + this.state.removeOtherSessionTokens(); + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].alerts.show({ + type: 'success' + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.session_terminated', { + count + })); + }).catch(() => { + _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].alerts.show({ + type: 'error' + }, _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].translator.trans('core.forum.security.session_termination_failed')); + }).finally(() => { + this.state.loadingTerminateSessions = false; + m.redraw(); + }); + } + globalLogout() { + this.state.loadingGlobalLogout = true; + return _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].request({ + method: 'POST', + url: _forum_app__WEBPACK_IMPORTED_MODULE_1__["default"].forum.attribute('baseUrl') + '/global-logout' + }).then(() => window.location.reload()).finally(() => { + this.state.loadingGlobalLogout = false; + m.redraw(); + }); + } +} +flarum.reg.add('core', 'forum/components/UserSecurityPage', UserSecurityPage); + +/***/ }), + +/***/ "./src/forum/states/UserSecurityPageState.ts": +/*!***************************************************!*\ + !*** ./src/forum/states/UserSecurityPageState.ts ***! + \***************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ UserSecurityPageState) +/* harmony export */ }); +/* harmony import */ var _babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/esm/defineProperty */ "../../../js-packages/webpack-config/node_modules/@babel/runtime/helpers/esm/defineProperty.js"); + +class UserSecurityPageState { + constructor() { + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "tokens", null); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "loadingTerminateSessions", false); + (0,_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__["default"])(this, "loadingGlobalLogout", false); + } + hasLoadedTokens() { + return this.tokens !== null; + } + getTokens() { + return this.tokens; + } + setTokens(tokens) { + this.tokens = tokens; + } + pushToken(token) { + var _this$tokens; + (_this$tokens = this.tokens) == null ? void 0 : _this$tokens.push(token); + } + removeToken(token) { + this.tokens = this.tokens.filter(t => t !== token); + } + getSessionTokens() { + var _this$tokens2; + return ((_this$tokens2 = this.tokens) == null ? void 0 : _this$tokens2.filter(token => token.isSessionToken()).sort((a, b) => b.isCurrent() ? 1 : -1)) || []; + } + getDeveloperTokens() { + var _this$tokens3; + return ((_this$tokens3 = this.tokens) == null ? void 0 : _this$tokens3.filter(token => !token.isSessionToken())) || null; + } + + /** + * Look up session tokens other than the current one. + */ + getOtherSessionTokens() { + var _this$tokens4; + return ((_this$tokens4 = this.tokens) == null ? void 0 : _this$tokens4.filter(token => token.isSessionToken() && !token.isCurrent())) || []; + } + hasOtherActiveSessions() { + return (this.getOtherSessionTokens() || []).length > 0; + } + removeOtherSessionTokens() { + this.tokens = this.tokens.filter(token => !token.isSessionToken() || token.isCurrent()); + } +} +flarum.reg.add('core', 'forum/states/UserSecurityPageState', UserSecurityPageState); + +/***/ }) + +}]); //# sourceMappingURL=UserSecurityPage.js.map \ No newline at end of file diff --git a/framework/core/js/dist/forum/components/UserSecurityPage.js.map b/framework/core/js/dist/forum/components/UserSecurityPage.js.map index 34c6d5ec56e..2b50532939b 100644 --- a/framework/core/js/dist/forum/components/UserSecurityPage.js.map +++ b/framework/core/js/dist/forum/components/UserSecurityPage.js.map @@ -1 +1 @@ -{"version":3,"file":"forum/components/UserSecurityPage.js","mappings":"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","sources":["webpack://@flarum/core/./src/common/components/LabelValue.tsx","webpack://@flarum/core/./src/forum/components/AccessTokensList.tsx","webpack://@flarum/core/./src/forum/components/NewAccessTokenModal.tsx","webpack://@flarum/core/./src/forum/states/UserSecurityPageState.ts","webpack://@flarum/core/./src/forum/components/UserSecurityPage.tsx"],"sourcesContent":["import Component from '../Component';\nimport app from '../app';\n/**\n * A generic component for displaying a label and value inline.\n * Created to avoid reinventing the wheel.\n *\n * `label: value`\n */\nexport default class LabelValue extends Component {\n view(vnode) {\n return m(\"div\", {\n className: \"LabelValue\"\n }, m(\"div\", {\n className: \"LabelValue-label\"\n }, app.translator.trans('core.lib.data_segment.label', {\n label: this.attrs.label\n })), m(\"div\", {\n className: \"LabelValue-value\"\n }, this.attrs.value));\n }\n}\nflarum.reg.add('core', 'common/components/LabelValue', LabelValue);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../app';\nimport Component from '../../common/Component';\nimport Button from '../../common/components/Button';\nimport humanTime from '../../common/helpers/humanTime';\nimport ItemList from '../../common/utils/ItemList';\nimport LabelValue from '../../common/components/LabelValue';\nimport extractText from '../../common/utils/extractText';\nimport classList from '../../common/utils/classList';\nimport Tooltip from '../../common/components/Tooltip';\nimport Icon from '../../common/components/Icon';\nexport default class AccessTokensList extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"loading\", {});\n _defineProperty(this, \"showingTokens\", {});\n }\n view(vnode) {\n return m(\"div\", {\n className: \"AccessTokensList\"\n }, this.attrs.tokens.length ? this.attrs.tokens.map(this.tokenView.bind(this)) : m(\"div\", {\n className: \"AccessTokensList--empty\"\n }, app.translator.trans('core.forum.security.empty_text')));\n }\n tokenView(token) {\n return m(\"div\", {\n className: classList('AccessTokensList-item', {\n 'AccessTokensList-item--active': token.isCurrent()\n }),\n key: token.id()\n }, this.tokenViewItems(token).toArray());\n }\n tokenViewItems(token) {\n const items = new ItemList();\n items.add('icon', m(\"div\", {\n className: \"AccessTokensList-item-icon\"\n }, m(Icon, {\n name: this.attrs.icon || 'fas fa-key'\n })), 50);\n items.add('info', m(\"div\", {\n className: \"AccessTokensList-item-info\"\n }, this.tokenInfoItems(token).toArray()), 40);\n items.add('actions', m(\"div\", {\n className: \"AccessTokensList-item-actions\"\n }, this.tokenActionItems(token).toArray()), 30);\n return items;\n }\n tokenInfoItems(token) {\n const items = new ItemList();\n if (this.attrs.type === 'session') {\n items.add('title', m(\"div\", {\n className: \"AccessTokensList-item-title\"\n }, m(\"span\", {\n className: \"AccessTokensList-item-title-main\"\n }, token.device()), token.isCurrent() && [' — ', m(\"span\", {\n className: \"AccessTokensList-item-title-sub\"\n }, app.translator.trans('core.forum.security.current_active_session'))]));\n } else {\n items.add('title', m(\"div\", {\n className: \"AccessTokensList-item-title\"\n }, m(\"span\", {\n className: \"AccessTokensList-item-title-main\"\n }, this.generateTokenTitle(token))));\n }\n items.add('createdAt', m(\"div\", {\n className: \"AccessTokensList-item-createdAt\"\n }, m(LabelValue, {\n label: app.translator.trans('core.forum.security.created'),\n value: humanTime(token.createdAt())\n })));\n items.add('lastActivityAt', m(\"div\", {\n className: \"AccessTokensList-item-lastActivityAt\"\n }, m(LabelValue, {\n label: app.translator.trans('core.forum.security.last_activity'),\n value: token.lastActivityAt() ? m('[', null, humanTime(token.lastActivityAt()), token.lastIpAddress() && \" \\u2014 \".concat(token.lastIpAddress()), this.attrs.type === 'developer_token' && token.device() && m('[', null, ' ', \"\\u2014 \", m(\"span\", {\n className: \"AccessTokensList-item-title-sub\"\n }, token.device()))) : app.translator.trans('core.forum.security.never')\n })));\n return items;\n }\n tokenActionItems(token) {\n const items = new ItemList();\n const deleteKey = {\n session: 'terminate_session',\n developer_token: 'revoke_access_token'\n }[this.attrs.type];\n if (this.attrs.type === 'developer_token') {\n const isHidden = !this.showingTokens[token.id()];\n const displayKey = isHidden ? 'show_access_token' : 'hide_access_token';\n items.add('toggleDisplay', m(Button, {\n className: \"Button Button--inverted\",\n icon: isHidden ? 'fas fa-eye' : 'fas fa-eye-slash',\n onclick: () => {\n this.showingTokens[token.id()] = isHidden;\n m.redraw();\n }\n }, app.translator.trans(\"core.forum.security.\".concat(displayKey))));\n }\n let revokeButton = m(Button, {\n className: \"Button Button--danger\",\n disabled: token.isCurrent(),\n loading: !!this.loading[token.id()],\n onclick: () => this.revoke(token)\n }, app.translator.trans(\"core.forum.security.\".concat(deleteKey)));\n if (token.isCurrent()) {\n revokeButton = m(Tooltip, {\n text: app.translator.trans('core.forum.security.cannot_terminate_current_session')\n }, m(\"div\", {\n tabindex: \"0\"\n }, revokeButton));\n }\n items.add('revoke', revokeButton);\n return items;\n }\n async revoke(token) {\n var _this$attrs$ondelete, _this$attrs;\n if (!confirm(extractText(app.translator.trans('core.forum.security.revoke_access_token_confirmation')))) return;\n this.loading[token.id()] = true;\n await token.delete();\n this.loading[token.id()] = false;\n (_this$attrs$ondelete = (_this$attrs = this.attrs).ondelete) == null ? void 0 : _this$attrs$ondelete.call(_this$attrs, token);\n const key = this.attrs.type === 'session' ? 'session_terminated' : 'token_revoked';\n app.alerts.show({\n type: 'success'\n }, app.translator.trans(\"core.forum.security.\".concat(key), {\n count: 1\n }));\n m.redraw();\n }\n generateTokenTitle(token) {\n const name = token.title() || app.translator.trans('core.forum.security.token_title_placeholder');\n const value = this.tokenValueDisplay(token);\n return app.translator.trans('core.forum.security.token_item_title', {\n title: name,\n token: value\n });\n }\n tokenValueDisplay(token) {\n const obfuscatedName = Array(12).fill('*').join('');\n const value = this.showingTokens[token.id()] ? token.token() : obfuscatedName;\n return m(\"code\", {\n className: \"AccessTokensList-item-token\"\n }, value);\n }\n}\nflarum.reg.add('core', 'forum/components/AccessTokensList', AccessTokensList);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport Stream from '../../common/utils/Stream';\nimport Form from '../../common/components/Form';\nexport default class NewAccessTokenModal extends FormModal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"titleInput\", Stream(''));\n }\n className() {\n return 'Modal--small NewAccessTokenModal';\n }\n title() {\n return app.translator.trans('core.forum.security.new_access_token_modal.title');\n }\n content() {\n const titleLabel = app.translator.trans('core.forum.security.new_access_token_modal.title_placeholder');\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n type: \"text\",\n className: \"FormControl\",\n bidi: this.titleInput,\n placeholder: titleLabel,\n \"aria-label\": titleLabel\n })), m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.security.new_access_token_modal.submit_button')))));\n }\n submitData() {\n return {\n title: this.titleInput()\n };\n }\n onsubmit(e) {\n super.onsubmit(e);\n e.preventDefault();\n this.loading = true;\n app.store.createRecord('access-tokens').save(this.submitData()).then(token => {\n this.attrs.onsuccess(token);\n app.modal.close();\n }).finally(this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/NewAccessTokenModal', NewAccessTokenModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nexport default class UserSecurityPageState {\n constructor() {\n _defineProperty(this, \"tokens\", null);\n _defineProperty(this, \"loadingTerminateSessions\", false);\n _defineProperty(this, \"loadingGlobalLogout\", false);\n }\n hasLoadedTokens() {\n return this.tokens !== null;\n }\n getTokens() {\n return this.tokens;\n }\n setTokens(tokens) {\n this.tokens = tokens;\n }\n pushToken(token) {\n var _this$tokens;\n (_this$tokens = this.tokens) == null ? void 0 : _this$tokens.push(token);\n }\n removeToken(token) {\n this.tokens = this.tokens.filter(t => t !== token);\n }\n getSessionTokens() {\n var _this$tokens2;\n return ((_this$tokens2 = this.tokens) == null ? void 0 : _this$tokens2.filter(token => token.isSessionToken()).sort((a, b) => b.isCurrent() ? 1 : -1)) || [];\n }\n getDeveloperTokens() {\n var _this$tokens3;\n return ((_this$tokens3 = this.tokens) == null ? void 0 : _this$tokens3.filter(token => !token.isSessionToken())) || null;\n }\n\n /**\n * Look up session tokens other than the current one.\n */\n getOtherSessionTokens() {\n var _this$tokens4;\n return ((_this$tokens4 = this.tokens) == null ? void 0 : _this$tokens4.filter(token => token.isSessionToken() && !token.isCurrent())) || [];\n }\n hasOtherActiveSessions() {\n return (this.getOtherSessionTokens() || []).length > 0;\n }\n removeOtherSessionTokens() {\n this.tokens = this.tokens.filter(token => !token.isSessionToken() || token.isCurrent());\n }\n}\nflarum.reg.add('core', 'forum/states/UserSecurityPageState', UserSecurityPageState);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport UserPage from './UserPage';\nimport ItemList from '../../common/utils/ItemList';\nimport FieldSet from '../../common/components/FieldSet';\nimport listItems from '../../common/helpers/listItems';\nimport extractText from '../../common/utils/extractText';\nimport AccessTokensList from './AccessTokensList';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport Button from '../../common/components/Button';\nimport NewAccessTokenModal from './NewAccessTokenModal';\nimport Tooltip from '../../common/components/Tooltip';\nimport UserSecurityPageState from '../states/UserSecurityPageState';\n\n/**\n * The `UserSecurityPage` component displays the user's security control panel, in\n * the context of their user profile.\n */\nexport default class UserSecurityPage extends UserPage {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"state\", new UserSecurityPageState());\n }\n oninit(vnode) {\n var _app$session$user;\n super.oninit(vnode);\n const routeUsername = m.route.param('username');\n if (routeUsername !== ((_app$session$user = app.session.user) == null ? void 0 : _app$session$user.slug()) && !app.forum.attribute('canModerateAccessTokens')) {\n m.route.set('/');\n }\n this.loadUser(routeUsername);\n app.setTitle(extractText(app.translator.trans('core.forum.security.title')));\n this.loadTokens();\n }\n content() {\n return m(\"div\", {\n className: \"UserSecurityPage\"\n }, m(\"ul\", null, listItems(this.settingsItems().toArray())));\n }\n\n /**\n * Build an item list for the user's settings controls.\n */\n settingsItems() {\n var _this$state$getDevelo;\n const items = new ItemList();\n if (app.forum.attribute('canCreateAccessToken') || app.forum.attribute('canModerateAccessTokens') || this.state.hasLoadedTokens() && (_this$state$getDevelo = this.state.getDeveloperTokens()) != null && _this$state$getDevelo.length) {\n items.add('developerTokens', m(FieldSet, {\n className: \"UserSecurityPage-developerTokens\",\n label: app.translator.trans(\"core.forum.security.developer_tokens_heading\")\n }, this.developerTokensItems().toArray()));\n } else if (!this.state.hasLoadedTokens()) {\n items.add('developerTokens', m(LoadingIndicator, null));\n }\n items.add('sessions', m(FieldSet, {\n className: \"UserSecurityPage-sessions\",\n label: app.translator.trans(\"core.forum.security.sessions_heading\")\n }, this.sessionsItems().toArray()));\n if (this.user.id() === app.session.user.id()) {\n items.add('globalLogout', m(FieldSet, {\n className: \"FieldSet--col UserSecurityPage-globalLogout\",\n label: app.translator.trans('core.forum.security.global_logout.heading'),\n description: app.translator.trans('core.forum.security.global_logout.help_text')\n }, m(Button, {\n className: \"Button\",\n icon: \"fas fa-sign-out-alt\",\n onclick: this.globalLogout.bind(this),\n loading: this.state.loadingGlobalLogout,\n disabled: this.state.loadingTerminateSessions\n }, app.translator.trans('core.forum.security.global_logout.log_out_button'))));\n }\n return items;\n }\n\n /**\n * Build an item list for the user's access accessToken settings.\n */\n developerTokensItems() {\n const items = new ItemList();\n items.add('accessTokenList', !this.state.hasLoadedTokens() ? m(LoadingIndicator, null) : m(AccessTokensList, {\n type: \"developer_token\",\n ondelete: token => {\n this.state.removeToken(token);\n m.redraw();\n },\n tokens: this.state.getDeveloperTokens(),\n icon: \"fas fa-key\",\n hideTokens: false\n }));\n if (this.user.id() === app.session.user.id()) {\n items.add('newAccessToken', m(\"div\", {\n className: \"UserSecurityPage-controls\"\n }, m(Button, {\n className: \"Button\",\n disabled: !app.forum.attribute('canCreateAccessToken'),\n onclick: () => app.modal.show(NewAccessTokenModal, {\n onsuccess: token => {\n this.state.pushToken(token);\n m.redraw();\n }\n })\n }, app.translator.trans('core.forum.security.new_access_token_button'))));\n }\n return items;\n }\n\n /**\n * Build an item list for the user's access accessToken settings.\n */\n sessionsItems() {\n const items = new ItemList();\n items.add('sessionsList', !this.state.hasLoadedTokens() ? m(LoadingIndicator, null) : m(AccessTokensList, {\n type: \"session\",\n ondelete: token => {\n this.state.removeToken(token);\n m.redraw();\n },\n tokens: this.state.getSessionTokens(),\n icon: \"fas fa-laptop\",\n hideTokens: true\n }));\n if (this.user.id() === app.session.user.id()) {\n const isDisabled = !this.state.hasOtherActiveSessions();\n let terminateAllOthersButton = m(Button, {\n className: \"Button\",\n onclick: this.terminateAllOtherSessions.bind(this),\n loading: this.state.loadingTerminateSessions,\n disabled: this.state.loadingGlobalLogout || isDisabled\n }, app.translator.trans('core.forum.security.terminate_all_other_sessions'));\n if (isDisabled) {\n terminateAllOthersButton = m(Tooltip, {\n text: app.translator.trans('core.forum.security.cannot_terminate_current_session')\n }, m(\"span\", {\n tabindex: \"0\"\n }, terminateAllOthersButton));\n }\n items.add('terminateAllOtherSessions', m(\"div\", {\n className: \"UserSecurityPage-controls\"\n }, terminateAllOthersButton));\n }\n return items;\n }\n loadTokens() {\n return app.store.find('access-tokens', {\n filter: {\n user: this.user.id()\n }\n }).then(tokens => {\n this.state.setTokens(tokens);\n m.redraw();\n });\n }\n terminateAllOtherSessions() {\n if (!confirm(extractText(app.translator.trans('core.forum.security.terminate_all_other_sessions_confirmation')))) return;\n this.state.loadingTerminateSessions = true;\n return app.request({\n method: 'DELETE',\n url: app.forum.attribute('apiUrl') + '/sessions'\n }).then(() => {\n // Count terminated sessions first.\n const count = this.state.getOtherSessionTokens().length;\n this.state.removeOtherSessionTokens();\n app.alerts.show({\n type: 'success'\n }, app.translator.trans('core.forum.security.session_terminated', {\n count\n }));\n }).catch(() => {\n app.alerts.show({\n type: 'error'\n }, app.translator.trans('core.forum.security.session_termination_failed'));\n }).finally(() => {\n this.state.loadingTerminateSessions = false;\n m.redraw();\n });\n }\n globalLogout() {\n this.state.loadingGlobalLogout = true;\n return app.request({\n method: 'POST',\n url: app.forum.attribute('baseUrl') + '/global-logout'\n }).then(() => window.location.reload()).finally(() => {\n this.state.loadingGlobalLogout = false;\n m.redraw();\n });\n }\n}\nflarum.reg.add('core', 'forum/components/UserSecurityPage', UserSecurityPage);"],"names":["LabelValue","Component","view","vnode","m","className","label","this","attrs","value","flarum","reg","add","AccessTokensList","constructor","super","arguments","tokens","length","map","tokenView","bind","app","token","classList","isCurrent","key","id","tokenViewItems","toArray","items","ItemList","Icon","name","icon","tokenInfoItems","tokenActionItems","type","device","generateTokenTitle","humanTime","createdAt","lastActivityAt","lastIpAddress","concat","deleteKey","session","developer_token","isHidden","showingTokens","displayKey","Button","onclick","redraw","revokeButton","disabled","loading","revoke","Tooltip","text","tabindex","async","_this$attrs$ondelete","_this$attrs","confirm","extractText","delete","ondelete","call","count","title","tokenValueDisplay","obfuscatedName","Array","fill","join","NewAccessTokenModal","FormModal","Stream","content","titleLabel","Form","bidi","titleInput","placeholder","submitData","onsubmit","e","preventDefault","save","then","onsuccess","finally","loaded","UserSecurityPageState","hasLoadedTokens","getTokens","setTokens","pushToken","_this$tokens","push","removeToken","filter","t","getSessionTokens","_this$tokens2","isSessionToken","sort","a","b","getDeveloperTokens","_this$tokens3","getOtherSessionTokens","_this$tokens4","hasOtherActiveSessions","removeOtherSessionTokens","UserSecurityPage","UserPage","oninit","_app$session$user","routeUsername","route","param","slug","set","loadUser","loadTokens","listItems","settingsItems","_this$state$getDevelo","state","FieldSet","developerTokensItems","LoadingIndicator","sessionsItems","user","description","globalLogout","loadingGlobalLogout","loadingTerminateSessions","hideTokens","isDisabled","terminateAllOthersButton","terminateAllOtherSessions","method","url","catch","window","location","reload"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"forum/components/UserSecurityPage.js","mappings":";;;;;;;;;;;;;;;AAAqC;AACZ;AACzB;AACA;AACA;AACA;AACA;AACA;AACe,yBAAyB,kDAAS;AACjD;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;ACrBwE;AAC/C;AACsB;AACK;AACG;AACJ;AACS;AACH;AACJ;AACC;AACN;AACjC,+BAA+B,yDAAS;AACvD;AACA;AACA,IAAI,qFAAe,oBAAoB;AACvC,IAAI,qFAAe,0BAA0B;AAC7C;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA;AACA;AACA,iBAAiB,mEAAS;AAC1B;AACA,OAAO;AACP;AACA,KAAK;AACL;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA,KAAK,IAAI,gEAAI;AACb;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA,OAAO,EAAE,6DAAoB;AAC7B,MAAM;AACN;AACA;AACA,OAAO;AACP;AACA,OAAO;AACP;AACA;AACA;AACA,KAAK,IAAI,qEAAU;AACnB,aAAa,6DAAoB;AACjC,aAAa,qEAAS;AACtB,KAAK;AACL;AACA;AACA,KAAK,IAAI,qEAAU;AACnB,aAAa,6DAAoB;AACjC,mDAAmD,qEAAS;AAC5D;AACA,OAAO,sBAAsB,6DAAoB;AACjD,KAAK;AACL;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,mCAAmC,iEAAM;AACzC;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,EAAE,6DAAoB;AAC7B;AACA,yBAAyB,iEAAM;AAC/B;AACA;AACA;AACA;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA,uBAAuB,kEAAO;AAC9B,cAAc,6DAAoB;AAClC,OAAO;AACP;AACA,OAAO;AACP;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB,qEAAW,CAAC,6DAAoB;AACjD;AACA;AACA;AACA;AACA;AACA,IAAI,wDAAe;AACnB;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA,KAAK;AACL;AACA;AACA;AACA,kCAAkC,6DAAoB;AACtD;AACA,WAAW,6DAAoB;AAC/B;AACA;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;;;;;;;;;;;;;;;;;;;;ACjJwE;AAC/C;AACiC;AACN;AACL;AACC;AACjC,kCAAkC,oEAAS;AAC1D;AACA;AACA,IAAI,qFAAe,qBAAqB,gEAAM;AAC9C;AACA;AACA;AACA;AACA;AACA,WAAW,6DAAoB;AAC/B;AACA;AACA,uBAAuB,6DAAoB;AAC3C;AACA;AACA,KAAK,IAAI,+DAAI;AACb;AACA,KAAK;AACL;AACA,KAAK;AACL;AACA;AACA;AACA;AACA;AACA,KAAK;AACL;AACA,KAAK,IAAI,iEAAM;AACf;AACA;AACA;AACA,KAAK,EAAE,6DAAoB;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,IAAI,+DAAsB;AAC1B;AACA,MAAM,wDAAe;AACrB,KAAK;AACL;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;ACtDwE;AACtC;AACA;AACiB;AACK;AACD;AACE;AACP;AACsB;AACpB;AACI;AACF;AACc;;AAEpE;AACA;AACA;AACA;AACe,+BAA+B,iDAAQ;AACtD;AACA;AACA,IAAI,qFAAe,oBAAoB,sEAAqB;AAC5D;AACA;AACA;AACA;AACA;AACA,gDAAgD,+DAAgB,mDAAmD,kEAAmB;AACtI;AACA;AACA;AACA,IAAI,2DAAY,CAAC,qEAAW,CAAC,mEAAoB;AACjD;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,qEAAS;AAC9B;;AAEA;AACA;AACA;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B,QAAQ,kEAAmB,4BAA4B,kEAAmB;AAC1E,qCAAqC,mEAAQ;AAC7C;AACA,eAAe,mEAAoB;AACnC,OAAO;AACP,MAAM;AACN,qCAAqC,2EAAgB;AACrD;AACA,4BAA4B,mEAAQ;AACpC;AACA,aAAa,mEAAoB;AACjC,KAAK;AACL,2BAA2B,kEAAmB;AAC9C,kCAAkC,mEAAQ;AAC1C;AACA,eAAe,mEAAoB;AACnC,qBAAqB,mEAAoB;AACzC,OAAO,IAAI,iEAAM;AACjB;AACA;AACA;AACA;AACA;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B,mEAAmE,2EAAgB,YAAY,yDAAgB;AAC/G;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,KAAK;AACL,2BAA2B,kEAAmB;AAC9C;AACA;AACA,OAAO,IAAI,iEAAM;AACjB;AACA,mBAAmB,kEAAmB;AACtC,uBAAuB,6DAAc,CAAC,6DAAmB;AACzD;AACA;AACA;AACA;AACA,SAAS;AACT,OAAO,EAAE,mEAAoB;AAC7B;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,sBAAsB,8DAAQ;AAC9B,gEAAgE,2EAAgB,YAAY,yDAAgB;AAC5G;AACA;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA,KAAK;AACL,2BAA2B,kEAAmB;AAC9C;AACA,uCAAuC,iEAAM;AAC7C;AACA;AACA;AACA;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA,qCAAqC,mEAAO;AAC5C,gBAAgB,mEAAoB;AACpC,SAAS;AACT;AACA,SAAS;AACT;AACA;AACA;AACA,OAAO;AACP;AACA;AACA;AACA;AACA,WAAW,6DAAc;AACzB;AACA;AACA;AACA,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA,iBAAiB,qEAAW,CAAC,mEAAoB;AACjD;AACA,WAAW,0DAAW;AACtB;AACA,WAAW,kEAAmB;AAC9B,KAAK;AACL;AACA;AACA;AACA,MAAM,8DAAe;AACrB;AACA,OAAO,EAAE,mEAAoB;AAC7B;AACA,OAAO;AACP,KAAK;AACL,MAAM,8DAAe;AACrB;AACA,OAAO,EAAE,mEAAoB;AAC7B,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;AACA,WAAW,0DAAW;AACtB;AACA,WAAW,kEAAmB;AAC9B,KAAK;AACL;AACA;AACA,KAAK;AACL;AACA;AACA;;;;;;;;;;;;;;;AC3LwE;AACzD;AACf;AACA,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB,IAAI,qFAAe;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://@flarum/core/./src/common/components/LabelValue.tsx","webpack://@flarum/core/./src/forum/components/AccessTokensList.tsx","webpack://@flarum/core/./src/forum/components/NewAccessTokenModal.tsx","webpack://@flarum/core/./src/forum/components/UserSecurityPage.tsx","webpack://@flarum/core/./src/forum/states/UserSecurityPageState.ts"],"sourcesContent":["import Component from '../Component';\nimport app from '../app';\n/**\n * A generic component for displaying a label and value inline.\n * Created to avoid reinventing the wheel.\n *\n * `label: value`\n */\nexport default class LabelValue extends Component {\n view(vnode) {\n return m(\"div\", {\n className: \"LabelValue\"\n }, m(\"div\", {\n className: \"LabelValue-label\"\n }, app.translator.trans('core.lib.data_segment.label', {\n label: this.attrs.label\n })), m(\"div\", {\n className: \"LabelValue-value\"\n }, this.attrs.value));\n }\n}\nflarum.reg.add('core', 'common/components/LabelValue', LabelValue);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../app';\nimport Component from '../../common/Component';\nimport Button from '../../common/components/Button';\nimport humanTime from '../../common/helpers/humanTime';\nimport ItemList from '../../common/utils/ItemList';\nimport LabelValue from '../../common/components/LabelValue';\nimport extractText from '../../common/utils/extractText';\nimport classList from '../../common/utils/classList';\nimport Tooltip from '../../common/components/Tooltip';\nimport Icon from '../../common/components/Icon';\nexport default class AccessTokensList extends Component {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"loading\", {});\n _defineProperty(this, \"showingTokens\", {});\n }\n view(vnode) {\n return m(\"div\", {\n className: \"AccessTokensList\"\n }, this.attrs.tokens.length ? this.attrs.tokens.map(this.tokenView.bind(this)) : m(\"div\", {\n className: \"AccessTokensList--empty\"\n }, app.translator.trans('core.forum.security.empty_text')));\n }\n tokenView(token) {\n return m(\"div\", {\n className: classList('AccessTokensList-item', {\n 'AccessTokensList-item--active': token.isCurrent()\n }),\n key: token.id()\n }, this.tokenViewItems(token).toArray());\n }\n tokenViewItems(token) {\n const items = new ItemList();\n items.add('icon', m(\"div\", {\n className: \"AccessTokensList-item-icon\"\n }, m(Icon, {\n name: this.attrs.icon || 'fas fa-key'\n })), 50);\n items.add('info', m(\"div\", {\n className: \"AccessTokensList-item-info\"\n }, this.tokenInfoItems(token).toArray()), 40);\n items.add('actions', m(\"div\", {\n className: \"AccessTokensList-item-actions\"\n }, this.tokenActionItems(token).toArray()), 30);\n return items;\n }\n tokenInfoItems(token) {\n const items = new ItemList();\n if (this.attrs.type === 'session') {\n items.add('title', m(\"div\", {\n className: \"AccessTokensList-item-title\"\n }, m(\"span\", {\n className: \"AccessTokensList-item-title-main\"\n }, token.device()), token.isCurrent() && [' — ', m(\"span\", {\n className: \"AccessTokensList-item-title-sub\"\n }, app.translator.trans('core.forum.security.current_active_session'))]));\n } else {\n items.add('title', m(\"div\", {\n className: \"AccessTokensList-item-title\"\n }, m(\"span\", {\n className: \"AccessTokensList-item-title-main\"\n }, this.generateTokenTitle(token))));\n }\n items.add('createdAt', m(\"div\", {\n className: \"AccessTokensList-item-createdAt\"\n }, m(LabelValue, {\n label: app.translator.trans('core.forum.security.created'),\n value: humanTime(token.createdAt())\n })));\n items.add('lastActivityAt', m(\"div\", {\n className: \"AccessTokensList-item-lastActivityAt\"\n }, m(LabelValue, {\n label: app.translator.trans('core.forum.security.last_activity'),\n value: token.lastActivityAt() ? m('[', null, humanTime(token.lastActivityAt()), token.lastIpAddress() && \" \\u2014 \".concat(token.lastIpAddress()), this.attrs.type === 'developer_token' && token.device() && m('[', null, ' ', \"\\u2014 \", m(\"span\", {\n className: \"AccessTokensList-item-title-sub\"\n }, token.device()))) : app.translator.trans('core.forum.security.never')\n })));\n return items;\n }\n tokenActionItems(token) {\n const items = new ItemList();\n const deleteKey = {\n session: 'terminate_session',\n developer_token: 'revoke_access_token'\n }[this.attrs.type];\n if (this.attrs.type === 'developer_token') {\n const isHidden = !this.showingTokens[token.id()];\n const displayKey = isHidden ? 'show_access_token' : 'hide_access_token';\n items.add('toggleDisplay', m(Button, {\n className: \"Button Button--inverted\",\n icon: isHidden ? 'fas fa-eye' : 'fas fa-eye-slash',\n onclick: () => {\n this.showingTokens[token.id()] = isHidden;\n m.redraw();\n }\n }, app.translator.trans(\"core.forum.security.\".concat(displayKey))));\n }\n let revokeButton = m(Button, {\n className: \"Button Button--danger\",\n disabled: token.isCurrent(),\n loading: !!this.loading[token.id()],\n onclick: () => this.revoke(token)\n }, app.translator.trans(\"core.forum.security.\".concat(deleteKey)));\n if (token.isCurrent()) {\n revokeButton = m(Tooltip, {\n text: app.translator.trans('core.forum.security.cannot_terminate_current_session')\n }, m(\"div\", {\n tabindex: \"0\"\n }, revokeButton));\n }\n items.add('revoke', revokeButton);\n return items;\n }\n async revoke(token) {\n var _this$attrs$ondelete, _this$attrs;\n if (!confirm(extractText(app.translator.trans('core.forum.security.revoke_access_token_confirmation')))) return;\n this.loading[token.id()] = true;\n await token.delete();\n this.loading[token.id()] = false;\n (_this$attrs$ondelete = (_this$attrs = this.attrs).ondelete) == null ? void 0 : _this$attrs$ondelete.call(_this$attrs, token);\n const key = this.attrs.type === 'session' ? 'session_terminated' : 'token_revoked';\n app.alerts.show({\n type: 'success'\n }, app.translator.trans(\"core.forum.security.\".concat(key), {\n count: 1\n }));\n m.redraw();\n }\n generateTokenTitle(token) {\n const name = token.title() || app.translator.trans('core.forum.security.token_title_placeholder');\n const value = this.tokenValueDisplay(token);\n return app.translator.trans('core.forum.security.token_item_title', {\n title: name,\n token: value\n });\n }\n tokenValueDisplay(token) {\n const obfuscatedName = Array(12).fill('*').join('');\n const value = this.showingTokens[token.id()] ? token.token() : obfuscatedName;\n return m(\"code\", {\n className: \"AccessTokensList-item-token\"\n }, value);\n }\n}\nflarum.reg.add('core', 'forum/components/AccessTokensList', AccessTokensList);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../app';\nimport FormModal from '../../common/components/FormModal';\nimport Button from '../../common/components/Button';\nimport Stream from '../../common/utils/Stream';\nimport Form from '../../common/components/Form';\nexport default class NewAccessTokenModal extends FormModal {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"titleInput\", Stream(''));\n }\n className() {\n return 'Modal--small NewAccessTokenModal';\n }\n title() {\n return app.translator.trans('core.forum.security.new_access_token_modal.title');\n }\n content() {\n const titleLabel = app.translator.trans('core.forum.security.new_access_token_modal.title_placeholder');\n return m(\"div\", {\n className: \"Modal-body\"\n }, m(Form, {\n className: \"Form--centered\"\n }, m(\"div\", {\n className: \"Form-group\"\n }, m(\"input\", {\n type: \"text\",\n className: \"FormControl\",\n bidi: this.titleInput,\n placeholder: titleLabel,\n \"aria-label\": titleLabel\n })), m(\"div\", {\n className: \"Form-group Form-controls\"\n }, m(Button, {\n className: \"Button Button--primary Button--block\",\n type: \"submit\",\n loading: this.loading\n }, app.translator.trans('core.forum.security.new_access_token_modal.submit_button')))));\n }\n submitData() {\n return {\n title: this.titleInput()\n };\n }\n onsubmit(e) {\n super.onsubmit(e);\n e.preventDefault();\n this.loading = true;\n app.store.createRecord('access-tokens').save(this.submitData()).then(token => {\n this.attrs.onsuccess(token);\n app.modal.close();\n }).finally(this.loaded.bind(this));\n }\n}\nflarum.reg.add('core', 'forum/components/NewAccessTokenModal', NewAccessTokenModal);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport app from '../../forum/app';\nimport UserPage from './UserPage';\nimport ItemList from '../../common/utils/ItemList';\nimport FieldSet from '../../common/components/FieldSet';\nimport listItems from '../../common/helpers/listItems';\nimport extractText from '../../common/utils/extractText';\nimport AccessTokensList from './AccessTokensList';\nimport LoadingIndicator from '../../common/components/LoadingIndicator';\nimport Button from '../../common/components/Button';\nimport NewAccessTokenModal from './NewAccessTokenModal';\nimport Tooltip from '../../common/components/Tooltip';\nimport UserSecurityPageState from '../states/UserSecurityPageState';\n\n/**\n * The `UserSecurityPage` component displays the user's security control panel, in\n * the context of their user profile.\n */\nexport default class UserSecurityPage extends UserPage {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"state\", new UserSecurityPageState());\n }\n oninit(vnode) {\n var _app$session$user;\n super.oninit(vnode);\n const routeUsername = m.route.param('username');\n if (routeUsername !== ((_app$session$user = app.session.user) == null ? void 0 : _app$session$user.slug()) && !app.forum.attribute('canModerateAccessTokens')) {\n m.route.set('/');\n }\n this.loadUser(routeUsername);\n app.setTitle(extractText(app.translator.trans('core.forum.security.title')));\n this.loadTokens();\n }\n content() {\n return m(\"div\", {\n className: \"UserSecurityPage\"\n }, m(\"ul\", null, listItems(this.settingsItems().toArray())));\n }\n\n /**\n * Build an item list for the user's settings controls.\n */\n settingsItems() {\n var _this$state$getDevelo;\n const items = new ItemList();\n if (app.forum.attribute('canCreateAccessToken') || app.forum.attribute('canModerateAccessTokens') || this.state.hasLoadedTokens() && (_this$state$getDevelo = this.state.getDeveloperTokens()) != null && _this$state$getDevelo.length) {\n items.add('developerTokens', m(FieldSet, {\n className: \"UserSecurityPage-developerTokens\",\n label: app.translator.trans(\"core.forum.security.developer_tokens_heading\")\n }, this.developerTokensItems().toArray()));\n } else if (!this.state.hasLoadedTokens()) {\n items.add('developerTokens', m(LoadingIndicator, null));\n }\n items.add('sessions', m(FieldSet, {\n className: \"UserSecurityPage-sessions\",\n label: app.translator.trans(\"core.forum.security.sessions_heading\")\n }, this.sessionsItems().toArray()));\n if (this.user.id() === app.session.user.id()) {\n items.add('globalLogout', m(FieldSet, {\n className: \"FieldSet--col UserSecurityPage-globalLogout\",\n label: app.translator.trans('core.forum.security.global_logout.heading'),\n description: app.translator.trans('core.forum.security.global_logout.help_text')\n }, m(Button, {\n className: \"Button\",\n icon: \"fas fa-sign-out-alt\",\n onclick: this.globalLogout.bind(this),\n loading: this.state.loadingGlobalLogout,\n disabled: this.state.loadingTerminateSessions\n }, app.translator.trans('core.forum.security.global_logout.log_out_button'))));\n }\n return items;\n }\n\n /**\n * Build an item list for the user's access accessToken settings.\n */\n developerTokensItems() {\n const items = new ItemList();\n items.add('accessTokenList', !this.state.hasLoadedTokens() ? m(LoadingIndicator, null) : m(AccessTokensList, {\n type: \"developer_token\",\n ondelete: token => {\n this.state.removeToken(token);\n m.redraw();\n },\n tokens: this.state.getDeveloperTokens(),\n icon: \"fas fa-key\",\n hideTokens: false\n }));\n if (this.user.id() === app.session.user.id()) {\n items.add('newAccessToken', m(\"div\", {\n className: \"UserSecurityPage-controls\"\n }, m(Button, {\n className: \"Button\",\n disabled: !app.forum.attribute('canCreateAccessToken'),\n onclick: () => app.modal.show(NewAccessTokenModal, {\n onsuccess: token => {\n this.state.pushToken(token);\n m.redraw();\n }\n })\n }, app.translator.trans('core.forum.security.new_access_token_button'))));\n }\n return items;\n }\n\n /**\n * Build an item list for the user's access accessToken settings.\n */\n sessionsItems() {\n const items = new ItemList();\n items.add('sessionsList', !this.state.hasLoadedTokens() ? m(LoadingIndicator, null) : m(AccessTokensList, {\n type: \"session\",\n ondelete: token => {\n this.state.removeToken(token);\n m.redraw();\n },\n tokens: this.state.getSessionTokens(),\n icon: \"fas fa-laptop\",\n hideTokens: true\n }));\n if (this.user.id() === app.session.user.id()) {\n const isDisabled = !this.state.hasOtherActiveSessions();\n let terminateAllOthersButton = m(Button, {\n className: \"Button\",\n onclick: this.terminateAllOtherSessions.bind(this),\n loading: this.state.loadingTerminateSessions,\n disabled: this.state.loadingGlobalLogout || isDisabled\n }, app.translator.trans('core.forum.security.terminate_all_other_sessions'));\n if (isDisabled) {\n terminateAllOthersButton = m(Tooltip, {\n text: app.translator.trans('core.forum.security.cannot_terminate_current_session')\n }, m(\"span\", {\n tabindex: \"0\"\n }, terminateAllOthersButton));\n }\n items.add('terminateAllOtherSessions', m(\"div\", {\n className: \"UserSecurityPage-controls\"\n }, terminateAllOthersButton));\n }\n return items;\n }\n loadTokens() {\n return app.store.find('access-tokens', {\n filter: {\n user: this.user.id()\n }\n }).then(tokens => {\n this.state.setTokens(tokens);\n m.redraw();\n });\n }\n terminateAllOtherSessions() {\n if (!confirm(extractText(app.translator.trans('core.forum.security.terminate_all_other_sessions_confirmation')))) return;\n this.state.loadingTerminateSessions = true;\n return app.request({\n method: 'DELETE',\n url: app.forum.attribute('apiUrl') + '/sessions'\n }).then(() => {\n // Count terminated sessions first.\n const count = this.state.getOtherSessionTokens().length;\n this.state.removeOtherSessionTokens();\n app.alerts.show({\n type: 'success'\n }, app.translator.trans('core.forum.security.session_terminated', {\n count\n }));\n }).catch(() => {\n app.alerts.show({\n type: 'error'\n }, app.translator.trans('core.forum.security.session_termination_failed'));\n }).finally(() => {\n this.state.loadingTerminateSessions = false;\n m.redraw();\n });\n }\n globalLogout() {\n this.state.loadingGlobalLogout = true;\n return app.request({\n method: 'POST',\n url: app.forum.attribute('baseUrl') + '/global-logout'\n }).then(() => window.location.reload()).finally(() => {\n this.state.loadingGlobalLogout = false;\n m.redraw();\n });\n }\n}\nflarum.reg.add('core', 'forum/components/UserSecurityPage', UserSecurityPage);","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nexport default class UserSecurityPageState {\n constructor() {\n _defineProperty(this, \"tokens\", null);\n _defineProperty(this, \"loadingTerminateSessions\", false);\n _defineProperty(this, \"loadingGlobalLogout\", false);\n }\n hasLoadedTokens() {\n return this.tokens !== null;\n }\n getTokens() {\n return this.tokens;\n }\n setTokens(tokens) {\n this.tokens = tokens;\n }\n pushToken(token) {\n var _this$tokens;\n (_this$tokens = this.tokens) == null ? void 0 : _this$tokens.push(token);\n }\n removeToken(token) {\n this.tokens = this.tokens.filter(t => t !== token);\n }\n getSessionTokens() {\n var _this$tokens2;\n return ((_this$tokens2 = this.tokens) == null ? void 0 : _this$tokens2.filter(token => token.isSessionToken()).sort((a, b) => b.isCurrent() ? 1 : -1)) || [];\n }\n getDeveloperTokens() {\n var _this$tokens3;\n return ((_this$tokens3 = this.tokens) == null ? void 0 : _this$tokens3.filter(token => !token.isSessionToken())) || null;\n }\n\n /**\n * Look up session tokens other than the current one.\n */\n getOtherSessionTokens() {\n var _this$tokens4;\n return ((_this$tokens4 = this.tokens) == null ? void 0 : _this$tokens4.filter(token => token.isSessionToken() && !token.isCurrent())) || [];\n }\n hasOtherActiveSessions() {\n return (this.getOtherSessionTokens() || []).length > 0;\n }\n removeOtherSessionTokens() {\n this.tokens = this.tokens.filter(token => !token.isSessionToken() || token.isCurrent());\n }\n}\nflarum.reg.add('core', 'forum/states/UserSecurityPageState', UserSecurityPageState);"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/framework/core/js/src/admin/AdminApplication.tsx b/framework/core/js/src/admin/AdminApplication.tsx index f3a15de8c62..28a9fc110cf 100644 --- a/framework/core/js/src/admin/AdminApplication.tsx +++ b/framework/core/js/src/admin/AdminApplication.tsx @@ -45,6 +45,7 @@ export interface AdminApplicationData extends ApplicationData { searchDrivers: Record; permissions: Record; maintenanceByConfig: boolean; + safeModeExtensions?: string[]; } export default class AdminApplication extends Application { diff --git a/framework/core/js/src/admin/components/AdminPage.tsx b/framework/core/js/src/admin/components/AdminPage.tsx index dcac3ed034b..ef6be4b35a0 100644 --- a/framework/core/js/src/admin/components/AdminPage.tsx +++ b/framework/core/js/src/admin/components/AdminPage.tsx @@ -9,6 +9,7 @@ import saveSettings from '../utils/saveSettings'; import AdminHeader from './AdminHeader'; import FormGroup, { FieldComponentOptions } from '../../common/components/FormGroup'; import extractText from '../../common/utils/extractText'; +import LoadingModal from './LoadingModal'; export interface AdminHeaderOptions { title: Mithril.Children; @@ -24,6 +25,8 @@ export interface AdminHeaderOptions { export type SettingsComponentOptions = FieldComponentOptions & { setting: string; + json?: boolean; + refreshAfterSaving?: boolean; }; /** @@ -38,6 +41,7 @@ export type SaveSubmitEvent = SubmitEvent & { redraw: boolean }; export default abstract class AdminPage extends Page { settings: MutableSettings = {}; + refreshAfterSaving: string[] = []; loading: boolean = false; view(vnode: Mithril.Vnode): Mithril.Children { @@ -137,9 +141,34 @@ export default abstract class AdminPage; + const originalBidi: (value?: string) => any = this.setting(setting); + let bidi: (value?: string) => any; + + if (json) { + bidi = function (value?: string) { + if (arguments.length) { + originalBidi(JSON.stringify(value)); + } + + const v = originalBidi(); + + if (v) { + return JSON.parse(v); + } + + return v; + }; + } else { + bidi = originalBidi; + } + + if (refreshAfterSaving) { + this.refreshAfterSaving.push(setting); + } + + return ; } /** @@ -194,7 +223,16 @@ export default abstract class AdminPage { + if (this.refreshAfterSaving.length && Object.keys(dirty).some((setting) => this.refreshAfterSaving.includes(setting))) { + app.modal.show(LoadingModal); + window.location.reload(); + } + }); } modelLocale(): Record { diff --git a/framework/core/js/src/admin/components/AdvancedPage.tsx b/framework/core/js/src/admin/components/AdvancedPage.tsx index 0b7584a346b..61b43c2166e 100644 --- a/framework/core/js/src/admin/components/AdvancedPage.tsx +++ b/framework/core/js/src/admin/components/AdvancedPage.tsx @@ -96,6 +96,9 @@ export default class AdvancedPage e } maintenance() { + const safeModeExtensionsByConfig = + JSON.stringify(app.data.safeModeExtensions) !== JSON.stringify(this.setting('safe_mode_extensions')()) ? app.data.safeModeExtensions : false; + return (
    @@ -103,6 +106,7 @@ export default class AdvancedPage e type: 'select', help: app.translator.trans('core.admin.advanced.maintenance.help'), setting: 'maintenance_mode', + refreshAfterSaving: true, options: { [MaintenanceMode.NO_MAINTENANCE]: app.translator.trans('core.admin.advanced.maintenance.options.0'), [MaintenanceMode.HIGH_MAINTENANCE]: { @@ -114,6 +118,27 @@ export default class AdvancedPage e }, default: 0, })} + {parseInt(this.setting('maintenance_mode')()) === MaintenanceMode.SAFE_MODE + ? this.buildSettingComponent({ + type: 'dropdown', + label: app.translator.trans('core.admin.advanced.maintenance.safe_mode_extensions'), + help: safeModeExtensionsByConfig + ? app.translator.trans('core.admin.advanced.maintenance.safe_mode_extensions_override_help', { + extensions: safeModeExtensionsByConfig.map((id) => app.data.extensions[id].extra['flarum-extension'].title).join(', '), + }) + : null, + setting: 'safe_mode_extensions', + json: true, + refreshAfterSaving: true, + multiple: true, + disabled: safeModeExtensionsByConfig, + options: Object.entries(app.data.extensions).reduce((acc, [id, extension]) => { + // @ts-ignore + acc[id] = extension.extra['flarum-extension'].title; + return acc; + }, {}), + }) + : null} {app.data.maintenanceByConfig ? (
    diff --git a/framework/core/js/src/admin/components/ExtensionPage.tsx b/framework/core/js/src/admin/components/ExtensionPage.tsx index d3e4327cf23..2e8c253bd45 100644 --- a/framework/core/js/src/admin/components/ExtensionPage.tsx +++ b/framework/core/js/src/admin/components/ExtensionPage.tsx @@ -63,7 +63,7 @@ export default class ExtensionPage {this.header()} - {app.data.maintenanceMode === MaintenanceMode.SAFE_MODE ? ( + {app.data.maintenanceMode === MaintenanceMode.SAFE_MODE && !app.data.safeModeExtensions?.includes(this.extension.id) ? (
    diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index a2f58dd75e2..ccb41f0ac48 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -279,25 +279,23 @@ export default class Application { public boot() { const caughtInitializationErrors: CallableFunction[] = []; - if (this.data.maintenanceMode !== MaintenanceMode.SAFE_MODE) { - this.initializers.toArray().forEach((initializer) => { - try { - initializer(this); - } catch (e) { - const extension = initializer.itemName.includes('/') - ? initializer.itemName.replace(/(\/flarum-ext-)|(\/flarum-)/g, '-') - : initializer.itemName; - - caughtInitializationErrors.push(() => - fireApplicationError( - extractText(app.translator.trans('core.lib.error.extension_initialiation_failed_message', { extension })), - `${extension} failed to initialize`, - e - ) - ); - } - }); - } + this.initializers.toArray().forEach((initializer) => { + try { + initializer(this); + } catch (e) { + const extension = initializer.itemName.includes('/') + ? initializer.itemName.replace(/(\/flarum-ext-)|(\/flarum-)/g, '-') + : initializer.itemName; + + caughtInitializationErrors.push(() => + fireApplicationError( + extractText(app.translator.trans('core.lib.error.extension_initialiation_failed_message', { extension })), + `${extension} failed to initialize`, + e + ) + ); + } + }); this.store.pushPayload({ data: this.data.resources }); diff --git a/framework/core/js/src/common/components/Dropdown.tsx b/framework/core/js/src/common/components/Dropdown.tsx index 31d0a74ab33..268159cc4bc 100644 --- a/framework/core/js/src/common/components/Dropdown.tsx +++ b/framework/core/js/src/common/components/Dropdown.tsx @@ -9,6 +9,8 @@ import Icon from './Icon'; export interface IDropdownAttrs extends ComponentAttrs { /** A class name to apply to the dropdown toggle button. */ buttonClassName?: string; + /** Additional attributes to apply to the dropdown toggle button. */ + buttonAttrs?: Record; /** A class name to apply to the dropdown menu. */ menuClassName?: string; /** The name of an icon to show in the dropdown toggle button. */ @@ -132,6 +134,7 @@ export default class Dropdown {this.getButtonContent(children)} diff --git a/framework/core/js/src/common/components/FormGroup.tsx b/framework/core/js/src/common/components/FormGroup.tsx index bb968a0d6b0..cfb71cb7d11 100644 --- a/framework/core/js/src/common/components/FormGroup.tsx +++ b/framework/core/js/src/common/components/FormGroup.tsx @@ -10,6 +10,7 @@ import ItemList from '../utils/ItemList'; import type { IUploadImageButtonAttrs } from './UploadImageButton'; import type { ComponentAttrs } from '../Component'; import type Mithril from 'mithril'; +import MultiSelect from './MultiSelect'; /** * A type that matches any valid value for the `type` attribute on an HTML `` element. @@ -81,8 +82,16 @@ export interface SelectFieldComponentOptions extends CommonFieldOptions { /** * Map of values to their labels */ - options: { [value: string]: Mithril.Children }; + options: { + [value: string]: + | Mithril.Children + | { + label: Mithril.Children; + disabled?: boolean; + }; + }; default: string; + multiple?: boolean; } /** @@ -122,7 +131,7 @@ export type FieldComponentOptions = export type IFormGroupAttrs = ComponentAttrs & FieldComponentOptions & { - bidi?: Stream; + stream?: Stream; }; /** @@ -157,12 +166,12 @@ export default class FormGroup): Mithril.Children { const customFieldComponents = this.customFieldComponents(); - const { help, type, label, bidi, ...componentAttrs } = this.attrs; + const { help, type, label, stream, ...componentAttrs } = this.attrs; // TypeScript being TypeScript - const attrs = componentAttrs as unknown as Omit; + const attrs = componentAttrs as unknown as Omit; - const value = bidi ? bidi() : null; + const value = stream ? stream() : null; const [inputId, helpTextId] = [generateElementId(), generateElementId()]; @@ -175,29 +184,31 @@ export default class FormGroup - + {label} {help ?
    {help}
    : null}
    ); } else if ((SelectSettingTypes as readonly string[]).includes(type)) { - const { default: defaultValue, options, ...otherAttrs } = attrs; + const { default: defaultValue, options, multiple, ...otherAttrs } = attrs; + + const Tag = multiple ? MultiSelect : Select; settingElement = ( -