From 3fd3900ceabf725f5fb609773b41b5f6faca0b73 Mon Sep 17 00:00:00 2001 From: fzngagan Date: Thu, 24 Sep 2020 17:34:32 +0530 Subject: [PATCH 1/8] Added message bus to wizard side --- assets/javascripts/wizard-custom.js | 3 +++ assets/javascripts/wizard/initializers/custom.js.es6 | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index c88cf204..0ff181d0 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -40,9 +40,12 @@ //= require discourse/app/lib/load-oneboxes //= require discourse/app/mixins/singleton +//= require discourse/app/mixins/upload //= require discourse/app/adapters/rest +//= require message-bus + //= require discourse/app/models/login-method //= require discourse/app/models/permission-type //= require discourse/app/models/archetype diff --git a/assets/javascripts/wizard/initializers/custom.js.es6 b/assets/javascripts/wizard/initializers/custom.js.es6 index 89bcf91c..dc1b400e 100644 --- a/assets/javascripts/wizard/initializers/custom.js.es6 +++ b/assets/javascripts/wizard/initializers/custom.js.es6 @@ -28,7 +28,7 @@ export default { const RestAdapter = requirejs("discourse/adapters/rest").default; const Session = requirejs("discourse/models/session").default; const setDefaultOwner = requirejs("discourse-common/lib/get-owner").setDefaultOwner; - + const messageBus = requirejs('message-bus-client').default; const container = app.__container__; Discourse.Model = EmberObject.extend(); Discourse.__container__ = container; @@ -62,7 +62,10 @@ export default { app.register("site-settings:main", siteSettings, { instantiate: false }); createHelperContext({ siteSettings }); targets.forEach(t => app.inject(t, "siteSettings", "site-settings:main")); - + + app.register("message-bus:main", messageBus, { instantiate: false }); + targets.forEach(t => app.inject(t, "messageBus", "message-bus:main")); + app.register("service:store", Store); targets.forEach(t => app.inject(t, "store", "service:store")); targets.forEach(t => app.inject(t, "appEvents", "service:app-events")); From 67d87f74c1c1c55bcd5e3e9dee881bcde0c30958 Mon Sep 17 00:00:00 2001 From: fzngagan Date: Thu, 1 Oct 2020 11:13:29 +0530 Subject: [PATCH 2/8] Upload files, drag and drop files, syntax highlighting working --- assets/javascripts/wizard-custom.js | 2 + .../components/wizard-composer-editor.js.es6 | 108 ++++-- .../wizard/initializers/custom.js.es6 | 314 ++++++++++-------- .../components/wizard-composer-editor.hbs | 4 +- .../stylesheets/wizard/custom/composer.scss | 4 + assets/stylesheets/wizard/wizard_custom.scss | 5 +- views/layouts/wizard.html.erb | 2 + 7 files changed, 271 insertions(+), 168 deletions(-) diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index 0ff181d0..03ab9103 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -38,6 +38,7 @@ //= require discourse/app/lib/cookie //= require discourse/app/lib/public-js-versions //= require discourse/app/lib/load-oneboxes +//= require discourse/app/lib/highlight-syntax //= require discourse/app/mixins/singleton //= require discourse/app/mixins/upload @@ -128,6 +129,7 @@ //= require template_include.js //= require caret_position.js //= require popper.js +//= require bootstrap-modal.js //= require bootbox.js //= require discourse-shims diff --git a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 index 50061883..7e2dce31 100644 --- a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 +++ b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 @@ -1,37 +1,41 @@ -import ComposerEditor from 'discourse/components/composer-editor'; -import { default as computed, on } from 'discourse-common/utils/decorators'; +import ComposerEditor from "discourse/components/composer-editor"; +import { default as computed, on } from "discourse-common/utils/decorators"; import { findRawTemplate } from "discourse-common/lib/raw-templates"; import { throttle } from "@ember/runloop"; import { scheduleOnce } from "@ember/runloop"; import { safariHacksDisabled } from "discourse/lib/utilities"; +import highlightSyntax from "discourse/lib/highlight-syntax"; +import { getToken } from "wizard/lib/ajax"; +import { validateUploadedFiles } from "discourse/lib/uploads"; +const uploadHandlers = []; export default ComposerEditor.extend({ - classNameBindings: ['fieldClass'], - allowUpload: false, + classNameBindings: ["fieldClass"], + allowUpload: true, showLink: false, topic: null, showToolbar: true, focusTarget: "reply", canWhisper: false, - lastValidatedAt: 'lastValidatedAt', + lastValidatedAt: "lastValidatedAt", uploadIcon: "upload", popupMenuOptions: [], - draftStatus: 'null', - + draftStatus: "null", + @on("didInsertElement") _composerEditorInit() { const $input = $(this.element.querySelector(".d-editor-input")); const $preview = $(this.element.querySelector(".d-editor-preview-wrapper")); - + if (this.siteSettings.enable_mentions) { $input.autocomplete({ template: findRawTemplate("user-selector-autocomplete"), - dataSource: term => this.userSearchTerm.call(this, term), + dataSource: (term) => this.userSearchTerm.call(this, term), key: "@", - transformComplete: v => v.username || v.name, + transformComplete: (v) => v.username || v.name, afterComplete() { scheduleOnce("afterRender", () => $input.blur().focus()); - } + }, }); } @@ -45,13 +49,73 @@ export default ComposerEditor.extend({ this._bindUploadTarget(); }, - + + showUploadModal() { + $(".wizard-composer-upload").trigger("click"); + }, + _setUploadPlaceholderSend() { + if (!this.composer.get("reply")) { + this.composer.set("reply", ""); + } + this._super(...arguments); + }, + _bindUploadTarget() { + this._super(...arguments); + const $element = $(this.element); + $element.off("fileuploadsubmit"); + $element.on("fileuploadsubmit", (e, data) => { + const max = this.siteSettings.simultaneous_uploads; + + // Limit the number of simultaneous uploads + if (max > 0 && data.files.length > max) { + bootbox.alert( + I18n.t("post.errors.too_many_dragged_and_dropped_files", { max }) + ); + return false; + } + + // Look for a matching file upload handler contributed from a plugin + const matcher = (handler) => { + const ext = handler.extensions.join("|"); + const regex = new RegExp(`\\.(${ext})$`, "i"); + return regex.test(data.files[0].name); + }; + + const matchingHandler = uploadHandlers.find(matcher); + if (data.files.length === 1 && matchingHandler) { + if (!matchingHandler.method(data.files[0], this)) { + return false; + } + } + + // If no plugin, continue as normal + const isPrivateMessage = this.get("composer.privateMessage"); + + data.formData = { type: "composer" }; + data.formData.authenticity_token = getToken(); + if (isPrivateMessage) { + data.formData.for_private_message = true; + } + if (this._pasted) { + data.formData.pasted = true; + } + + const opts = { + user: this.currentUser, + siteSettings: this.siteSettings, + isPrivateMessage, + allowStaffToUploadAnyFileInPm: this.siteSettings + .allow_staff_to_upload_any_file_in_pm, + }; + + const isUploading = validateUploadedFiles(data.files, opts); + + this.setProperties({ uploadProgress: 0, isUploading }); + + return isUploading; + }); }, - - _unbindUploadTarget() { - }, - actions: { extraButtons(toolbar) { if (this.allowUpload && this.uploadIcon && !this.site.mobileView) { @@ -60,9 +124,13 @@ export default ComposerEditor.extend({ group: "insertions", icon: this.uploadIcon, title: "upload", - sendAction: this.showUploadModal + sendAction: this.showUploadModal, }); } - } - } -}) \ No newline at end of file + }, + previewUpdated($preview) { + highlightSyntax($preview[0], this.siteSettings, this.session); + this._super(...arguments); + }, + }, +}); diff --git a/assets/javascripts/wizard/initializers/custom.js.es6 b/assets/javascripts/wizard/initializers/custom.js.es6 index dc1b400e..60f9a0e2 100644 --- a/assets/javascripts/wizard/initializers/custom.js.es6 +++ b/assets/javascripts/wizard/initializers/custom.js.es6 @@ -1,34 +1,45 @@ -import { default as computed } from 'discourse-common/utils/decorators'; +import { default as computed } from "discourse-common/utils/decorators"; import { dasherize } from "@ember/string"; export default { - name: 'custom-routes', - + name: "custom-routes", + after: "sniff-capabilities", initialize(app) { - if (window.location.pathname.indexOf('/w/') < 0) return; + if (window.location.pathname.indexOf("/w/") < 0) return; - const EmberObject = requirejs('@ember/object').default; - const Router = requirejs('wizard/router').default; - const ApplicationRoute = requirejs('wizard/routes/application').default; - const ajax = requirejs('wizard/lib/ajax').ajax; - const StepModel = requirejs('wizard/models/step').default; - const CustomWizard = requirejs('discourse/plugins/discourse-custom-wizard/wizard/models/custom').default; - const WizardStep = requirejs('wizard/components/wizard-step').default; - const WizardField = requirejs('wizard/components/wizard-field').default; - const getUrl = requirejs('discourse-common/lib/get-url').default; - const FieldModel = requirejs('wizard/models/wizard-field').default; - const autocomplete = requirejs('discourse/lib/autocomplete').default; - const cook = requirejs('discourse/plugins/discourse-custom-wizard/wizard/lib/text-lite').cook; + const EmberObject = requirejs("@ember/object").default; + const Router = requirejs("wizard/router").default; + const ApplicationRoute = requirejs("wizard/routes/application").default; + const ajax = requirejs("wizard/lib/ajax").ajax; + const StepModel = requirejs("wizard/models/step").default; + const CustomWizard = requirejs( + "discourse/plugins/discourse-custom-wizard/wizard/models/custom" + ).default; + const WizardStep = requirejs("wizard/components/wizard-step").default; + const WizardField = requirejs("wizard/components/wizard-field").default; + const getUrl = requirejs("discourse-common/lib/get-url").default; + const FieldModel = requirejs("wizard/models/wizard-field").default; + const autocomplete = requirejs("discourse/lib/autocomplete").default; + const cook = requirejs( + "discourse/plugins/discourse-custom-wizard/wizard/lib/text-lite" + ).cook; const Singleton = requirejs("discourse/mixins/singleton").default; const Store = requirejs("discourse/models/store").default; - const registerRawHelpers = requirejs("discourse-common/lib/raw-handlebars-helpers").registerRawHelpers; - const createHelperContext = requirejs("discourse-common/lib/helpers").createHelperContext; - const RawHandlebars = requirejs("discourse-common/lib/raw-handlebars").default; - const Site = requirejs("discourse/plugins/discourse-custom-wizard/wizard/models/site").default; + const registerRawHelpers = requirejs( + "discourse-common/lib/raw-handlebars-helpers" + ).registerRawHelpers; + const createHelperContext = requirejs("discourse-common/lib/helpers") + .createHelperContext; + const RawHandlebars = requirejs("discourse-common/lib/raw-handlebars") + .default; + const Site = requirejs( + "discourse/plugins/discourse-custom-wizard/wizard/models/site" + ).default; const RestAdapter = requirejs("discourse/adapters/rest").default; const Session = requirejs("discourse/models/session").default; - const setDefaultOwner = requirejs("discourse-common/lib/get-owner").setDefaultOwner; - const messageBus = requirejs('message-bus-client').default; + const setDefaultOwner = requirejs("discourse-common/lib/get-owner") + .setDefaultOwner; + const messageBus = requirejs("message-bus-client").default; const container = app.__container__; Discourse.Model = EmberObject.extend(); Discourse.__container__ = container; @@ -37,20 +48,19 @@ export default { // IE11 Polyfill - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries#Polyfill if (!Object.entries) - Object.entries = function( obj ){ - var ownProps = Object.keys( obj ), - i = ownProps.length, - resArray = new Array(i); // preallocate the Array - while (i--) - resArray[i] = [ownProps[i], obj[ownProps[i]]]; + Object.entries = function (obj) { + var ownProps = Object.keys(obj), + i = ownProps.length, + resArray = new Array(i); // preallocate the Array + while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]]; return resArray; }; $.fn.autocomplete = autocomplete; - - Object.keys(Ember.TEMPLATES).forEach(k => { - if (k.indexOf("select-kit") === 0) { + + Object.keys(Ember.TEMPLATES).forEach((k) => { + if (k.indexOf("select-kit") === 0) { let template = Ember.TEMPLATES[k]; define(k, () => template); } @@ -61,148 +71,162 @@ export default { const siteSettings = Wizard.SiteSettings; app.register("site-settings:main", siteSettings, { instantiate: false }); createHelperContext({ siteSettings }); - targets.forEach(t => app.inject(t, "siteSettings", "site-settings:main")); + targets.forEach((t) => app.inject(t, "siteSettings", "site-settings:main")); app.register("message-bus:main", messageBus, { instantiate: false }); - targets.forEach(t => app.inject(t, "messageBus", "message-bus:main")); + targets.forEach((t) => app.inject(t, "messageBus", "message-bus:main")); app.register("service:store", Store); - targets.forEach(t => app.inject(t, "store", "service:store")); - targets.forEach(t => app.inject(t, "appEvents", "service:app-events")); - + targets.forEach((t) => app.inject(t, "store", "service:store")); + targets.forEach((t) => app.inject(t, "appEvents", "service:app-events")); + app.register("adapter:rest", RestAdapter); - + const site = Site.current(); app.register("site:main", site, { instantiate: false }); - targets.forEach(t => app.inject(t, "site", "site:main")); - - site.set('can_create_tag', false); - + targets.forEach((t) => app.inject(t, "site", "site:main")); + + site.set("can_create_tag", false); + app.register("session:main", Session.current(), { instantiate: false }); + targets.forEach((t) => app.inject(t, "session", "session:main")); let context = { siteSettings: container.lookup("site-settings:main"), currentUser: container.lookup("current-user:main"), site: container.lookup("site:main"), session: container.lookup("session:main"), + capabilities: container.lookup("capabilities:main"), }; createHelperContext(context); - + const session = container.lookup("session:main"); + const setupData = document.getElementById("data-discourse-setup").dataset; + session.set("highlightJsPath", setupData.highlightJsPath); Router.reopen({ - rootURL: getUrl('/w/') + rootURL: getUrl("/w/"), }); - Router.map(function() { - this.route('custom', { path: '/:wizard_id' }, function() { - this.route('steps'); - this.route('step', { path: '/steps/:step_id' }); + Router.map(function () { + this.route("custom", { path: "/:wizard_id" }, function () { + this.route("steps"); + this.route("step", { path: "/steps/:step_id" }); }); }); ApplicationRoute.reopen({ redirect() { - this.transitionTo('custom'); + this.transitionTo("custom"); }, - model() {} + model() {}, }); WizardStep.reopen({ - classNameBindings: ['step.id'], + classNameBindings: ["step.id"], animateInvalidFields() { - Ember.run.scheduleOnce('afterRender', () => { - let $element = $('.invalid input[type=text], .invalid textarea, .invalid input[type=checkbox], .invalid .select-kit'); - + Ember.run.scheduleOnce("afterRender", () => { + let $element = $( + ".invalid input[type=text], .invalid textarea, .invalid input[type=checkbox], .invalid .select-kit" + ); + if ($element.length) { - $([document.documentElement, document.body]).animate({ - scrollTop: $element.offset().top - 200 - }, 400, function() { - $element.wiggle(2, 100); - }); + $([document.documentElement, document.body]).animate( + { + scrollTop: $element.offset().top - 200, + }, + 400, + function () { + $element.wiggle(2, 100); + } + ); } }); }, - ensureStartsAtTop: function() { - window.scrollTo(0,0); - }.observes('step.id'), + ensureStartsAtTop: function () { + window.scrollTo(0, 0); + }.observes("step.id"), - showQuitButton: function() { - const index = this.get('step.index'); - const required = this.get('wizard.required'); + showQuitButton: function () { + const index = this.get("step.index"); + const required = this.get("wizard.required"); return index === 0 && !required; - }.property('step.index', 'wizard.required'), + }.property("step.index", "wizard.required"), - cookedTitle: function() { - return cook(this.get('step.title')); - }.property('step.title'), + cookedTitle: function () { + return cook(this.get("step.title")); + }.property("step.title"), - cookedDescription: function() { - return cook(this.get('step.description')); - }.property('step.description'), + cookedDescription: function () { + return cook(this.get("step.description")); + }.property("step.description"), - bannerImage: function() { - const src = this.get('step.banner'); + bannerImage: function () { + const src = this.get("step.banner"); if (!src) return; return getUrl(src); - }.property('step.banner'), + }.property("step.banner"), - handleMessage: function() { - const message = this.get('step.message'); - this.sendAction('showMessage', message); - }.observes('step.message'), + handleMessage: function () { + const message = this.get("step.message"); + this.sendAction("showMessage", message); + }.observes("step.message"), advance() { - this.set('saving', true); - this.get('step').save() - .then(response => { - if (this.get('finalStep')) { + this.set("saving", true); + this.get("step") + .save() + .then((response) => { + if (this.get("finalStep")) { CustomWizard.finished(response); } else { - this.sendAction('goNext', response); + this.sendAction("goNext", response); } }) .catch(() => this.animateInvalidFields()) - .finally(() => this.set('saving', false)); - }, - - keyPress(key) { + .finally(() => this.set("saving", false)); }, + keyPress(key) {}, + actions: { quit() { - this.get('wizard').skip(); + this.get("wizard").skip(); }, done() { - this.set('finalStep', true); - this.send('nextStep'); + this.set("finalStep", true); + this.send("nextStep"); }, showMessage(message) { - this.sendAction('showMessage', message); - } - } + this.sendAction("showMessage", message); + }, + }, }); StepModel.reopen({ save() { - const wizardId = this.get('wizardId'); + const wizardId = this.get("wizardId"); const fields = {}; - this.get('fields').forEach(f => { - if (f.type !== 'text_only') { + this.get("fields").forEach((f) => { + if (f.type !== "text_only") { fields[f.id] = f.value; } }); - + return ajax({ - url: `/w/${wizardId}/steps/${this.get('id')}`, - type: 'PUT', - data: { fields } - }).catch(response => { - if (response && response.responseJSON && response.responseJSON.errors) { + url: `/w/${wizardId}/steps/${this.get("id")}`, + type: "PUT", + data: { fields }, + }).catch((response) => { + if ( + response && + response.responseJSON && + response.responseJSON.errors + ) { let wizardErrors = []; - response.responseJSON.errors.forEach(err => { + response.responseJSON.errors.forEach((err) => { if (err.field === wizardId) { wizardErrors.push(err.description); } else if (err.field) { @@ -212,7 +236,7 @@ export default { } }); if (wizardErrors.length) { - this.handleWizardError(wizardErrors.join('\n')); + this.handleWizardError(wizardErrors.join("\n")); } this.animateInvalidFields(); throw response; @@ -220,8 +244,8 @@ export default { if (response && response.responseText) { const responseText = response.responseText; - const start = responseText.indexOf('>') + 1; - const end = responseText.indexOf('plugins'); + const start = responseText.indexOf(">") + 1; + const end = responseText.indexOf("plugins"); const message = responseText.substring(start, end); this.handleWizardError(message); throw message; @@ -230,44 +254,44 @@ export default { }, handleWizardError(message) { - this.set('message', { - state: 'error', - text: message + this.set("message", { + state: "error", + text: message, }); - Ember.run.later(() => this.set('message', null), 6000); - } + Ember.run.later(() => this.set("message", null), 6000); + }, }); WizardField.reopen({ - classNameBindings: ['field.id'], + classNameBindings: ["field.id"], - cookedDescription: function() { - return cook(this.get('field.description')); - }.property('field.description'), + cookedDescription: function () { + return cook(this.get("field.description")); + }.property("field.description"), - inputComponentName: function() { - const type = this.get('field.type'); - const id = this.get('field.id'); - if (['text_only'].includes(type)) return false; - return dasherize((type === 'component') ? id : `wizard-field-${type}`); - }.property('field.type', 'field.id') + inputComponentName: function () { + const type = this.get("field.type"); + const id = this.get("field.id"); + if (["text_only"].includes(type)) return false; + return dasherize(type === "component" ? id : `wizard-field-${type}`); + }.property("field.type", "field.id"), }); const StandardFieldValidation = [ - 'text', - 'number', - 'textarea', - 'dropdown', - 'tag', - 'image', - 'user_selector', - 'text_only', - 'composer', - 'category', - 'group', - 'date', - 'time', - 'date_time' + "text", + "number", + "textarea", + "dropdown", + "tag", + "image", + "user_selector", + "text_only", + "composer", + "category", + "group", + "date", + "time", + "date_time", ]; FieldModel.reopen({ @@ -275,7 +299,7 @@ export default { if (this.customCheck) { return this.customCheck(); } - + let valid = this.valid; if (!this.required) { @@ -283,23 +307,23 @@ export default { return true; } - const val = this.get('value'); - const type = this.get('type'); - - if (type === 'checkbox') { + const val = this.get("value"); + const type = this.get("type"); + + if (type === "checkbox") { valid = val; - } else if (type === 'upload') { + } else if (type === "upload") { valid = val && val.id > 0; } else if (StandardFieldValidation.indexOf(type) > -1) { valid = val && val.toString().length > 0; - } else if (type === 'url') { + } else if (type === "url") { valid = true; } - + this.setValid(valid); return valid; - } + }, }); - } + }, }; diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs index 87af2ccb..5e6e3c8c 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs @@ -13,4 +13,6 @@ showLink=showLink composerEvents=true disabled=disableTextarea - outletArgs=(hash composer=composer editorType="composer")}} \ No newline at end of file + outletArgs=(hash composer=composer editorType="composer")}} + + {{input class="wizard-composer-upload hidden-upload-field" disabled=uploading type="file" multiple=true }} diff --git a/assets/stylesheets/wizard/custom/composer.scss b/assets/stylesheets/wizard/custom/composer.scss index 08521893..ddbdc4f7 100644 --- a/assets/stylesheets/wizard/custom/composer.scss +++ b/assets/stylesheets/wizard/custom/composer.scss @@ -183,3 +183,7 @@ .d-editor-button-bar .btn { border: none; } + +.wizard-composer-upload { + display: none; +} diff --git a/assets/stylesheets/wizard/wizard_custom.scss b/assets/stylesheets/wizard/wizard_custom.scss index 366981bc..9e2221fc 100644 --- a/assets/stylesheets/wizard/wizard_custom.scss +++ b/assets/stylesheets/wizard/wizard_custom.scss @@ -1,5 +1,8 @@ @import "common/foundation/colors"; @import "common/foundation/variables"; +@import "common/base/code_highlighting"; +@import "common/base/modal"; +@import "desktop/modal"; @import "custom/base"; @import "custom/wizard"; @import "custom/step"; @@ -9,5 +12,3 @@ @import "custom/composer"; @import "custom/events"; @import "custom/locations"; - - diff --git a/views/layouts/wizard.html.erb b/views/layouts/wizard.html.erb index 15a902b9..5979a464 100644 --- a/views/layouts/wizard.html.erb +++ b/views/layouts/wizard.html.erb @@ -23,6 +23,8 @@ <%= server_plugin_outlet "custom_wizard" %> + <%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %> + "> From 3bd1129acf7a913c17da2f8df131b178e762324d Mon Sep 17 00:00:00 2001 From: fzngagan Date: Thu, 1 Oct 2020 19:18:20 +0530 Subject: [PATCH 3/8] FEATURE: Insert hyperlink on wizard composer --- .../components/wizard-composer-editor.js.es6 | 18 ++++++++++++++++++ .../wizard-composer-hyperlink.js.es6 | 12 ++++++++++++ .../components/wizard-composer-editor.hbs | 6 ++++++ .../components/wizard-composer-hyperlink.hbs | 9 +++++++++ assets/stylesheets/wizard/custom/composer.scss | 8 ++++++++ config/locales/client.en.yml | 7 +++++++ 6 files changed, 60 insertions(+) create mode 100644 assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 create mode 100644 assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs diff --git a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 index 7e2dce31..26207c89 100644 --- a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 +++ b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 @@ -13,6 +13,7 @@ export default ComposerEditor.extend({ classNameBindings: ["fieldClass"], allowUpload: true, showLink: false, + showHyperlinkBox: false, topic: null, showToolbar: true, focusTarget: "reply", @@ -127,10 +128,27 @@ export default ComposerEditor.extend({ sendAction: this.showUploadModal, }); } + + const component = this; + toolbar.addButton({ + id: "link", + group: "insertions", + shortcut: "K", + trimLeading: true, + sendAction: (event) => component.set("showHyperlinkBox", true), + }); }, previewUpdated($preview) { highlightSyntax($preview[0], this.siteSettings, this.session); this._super(...arguments); }, + addLink(linkName, linkUrl) { + let link = `[${linkName}](${linkUrl})`; + this.appEvents.trigger("composer:insert-text", link); + this.set("showHyperlinkBox", false); + }, + hideBox() { + this.set("showHyperlinkBox", false); + }, }, }); diff --git a/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 b/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 new file mode 100644 index 00000000..6e9a36a4 --- /dev/null +++ b/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 @@ -0,0 +1,12 @@ +import Component from "@ember/component"; + +export default Component.extend({ + actions: { + addLink() { + this.addLink(this.linkName, this.linkUrl); + }, + hideBox() { + this.hideBox(); + }, + }, +}); diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs index 5e6e3c8c..c3bfdb6e 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs @@ -16,3 +16,9 @@ outletArgs=(hash composer=composer editorType="composer")}} {{input class="wizard-composer-upload hidden-upload-field" disabled=uploading type="file" multiple=true }} + + {{#if showHyperlinkBox}} + {{wizard-composer-hyperlink + addLink=(action 'addLink') + hideBox=(action 'hideBox')}} + {{/if}} diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs b/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs new file mode 100644 index 00000000..df794619 --- /dev/null +++ b/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs @@ -0,0 +1,9 @@ +

{{i18n "wizard_composer.insert_hyperlink.heading"}}

+{{input class="composer-link-name" placeholder=(i18n 'wizard_composer.insert_hyperlink.placeholder.link_name') type="text" value=linkName}} +{{input class="composer-link-url" placeholder=(i18n 'wizard_composer.insert_hyperlink.placeholder.link_url') type="text" value=linkUrl}} +{{d-button label="wizard_composer.insert_hyperlink.ok" + class="add-link" + click=(action 'addLink')}} +{{d-button label="wizard_composer.insert_hyperlink.cancel" + class="hide-hyperlink-box" + click=(action 'hideBox')}} diff --git a/assets/stylesheets/wizard/custom/composer.scss b/assets/stylesheets/wizard/custom/composer.scss index ddbdc4f7..a0d489a2 100644 --- a/assets/stylesheets/wizard/custom/composer.scss +++ b/assets/stylesheets/wizard/custom/composer.scss @@ -187,3 +187,11 @@ .wizard-composer-upload { display: none; } + +.composer-link-url, .composer-link-name { + display: block; +} + +.hide-hyperlink-box, .add-link { + display: inline; +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 65e31c96..2766790d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -455,3 +455,10 @@ en: yourself_confirm: title: "Did you forget to add recipients?" body: "Right now this message is only being sent to yourself!" + insert_hyperlink: + heading: "Insert Hyperlink" + ok: "Ok" + cancel: "Cancel" + placeholder: + link_name: "Link Text" + link_url: "Link Url" From aa7d5df77b34766eee7b6f2c16fc15204e18b9a3 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Fri, 2 Oct 2020 10:33:01 +1000 Subject: [PATCH 4/8] CLEANUP #1: Upload progress and link modal --- .../components/wizard-composer-editor.js.es6 | 33 ++++++++++++++++ .../wizard-composer-hyperlink.js.es6 | 5 ++- .../components/wizard-composer-editor.hbs | 23 ++++++++--- .../components/wizard-composer-hyperlink.hbs | 30 +++++++++----- .../components/wizard-field-composer.hbs | 15 +++---- .../stylesheets/wizard/custom/composer.scss | 39 ++++++++++++++++++- assets/stylesheets/wizard/wizard_custom.scss | 3 ++ 7 files changed, 123 insertions(+), 25 deletions(-) diff --git a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 index 26207c89..2b745c6f 100644 --- a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 +++ b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 @@ -54,6 +54,7 @@ export default ComposerEditor.extend({ showUploadModal() { $(".wizard-composer-upload").trigger("click"); }, + _setUploadPlaceholderSend() { if (!this.composer.get("reply")) { this.composer.set("reply", ""); @@ -64,7 +65,9 @@ export default ComposerEditor.extend({ _bindUploadTarget() { this._super(...arguments); const $element = $(this.element); + $element.off("fileuploadsubmit"); + $element.on("fileuploadsubmit", (e, data) => { const max = this.siteSettings.simultaneous_uploads; @@ -116,7 +119,33 @@ export default ComposerEditor.extend({ return isUploading; }); + + $element.on("fileuploadprogressall", (e, data) => { + this.set( + "uploadProgress", + parseInt((data.loaded / data.total) * 100, 10) + ); + }); + + $element.on("fileuploadfail", (e, data) => { + this._setUploadPlaceholderDone(data); + this._resetUpload(true); + + const userCancelled = this._xhr && this._xhr._userCancelled; + this._xhr = null; + + if (!userCancelled) { + displayErrorForUpload(data, this.siteSettings); + } + }); }, + + click(e) { + if ($(e.target).hasClass('wizard-composer-hyperlink')) { + this.set('showHyperlinkBox', false); + } + }, + actions: { extraButtons(toolbar) { if (this.allowUpload && this.uploadIcon && !this.site.mobileView) { @@ -135,18 +164,22 @@ export default ComposerEditor.extend({ group: "insertions", shortcut: "K", trimLeading: true, + unshift: true, sendAction: (event) => component.set("showHyperlinkBox", true), }); }, + previewUpdated($preview) { highlightSyntax($preview[0], this.siteSettings, this.session); this._super(...arguments); }, + addLink(linkName, linkUrl) { let link = `[${linkName}](${linkUrl})`; this.appEvents.trigger("composer:insert-text", link); this.set("showHyperlinkBox", false); }, + hideBox() { this.set("showHyperlinkBox", false); }, diff --git a/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 b/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 index 6e9a36a4..5cc0357d 100644 --- a/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 +++ b/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 @@ -1,12 +1,15 @@ import Component from "@ember/component"; export default Component.extend({ + classNames: ['wizard-composer-hyperlink'], + actions: { addLink() { this.addLink(this.linkName, this.linkUrl); }, + hideBox() { this.hideBox(); - }, + } }, }); diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs index c3bfdb6e..3194186e 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs @@ -15,10 +15,21 @@ disabled=disableTextarea outletArgs=(hash composer=composer editorType="composer")}} - {{input class="wizard-composer-upload hidden-upload-field" disabled=uploading type="file" multiple=true }} +{{input + class="wizard-composer-upload hidden-upload-field" + disabled=uploading + type="file" + multiple=true}} - {{#if showHyperlinkBox}} - {{wizard-composer-hyperlink - addLink=(action 'addLink') - hideBox=(action 'hideBox')}} - {{/if}} +{{#if showHyperlinkBox}} + {{wizard-composer-hyperlink + addLink=(action 'addLink') + hideBox=(action 'hideBox')}} +{{/if}} + +
+ {{loading-spinner size="small"}}{{i18n "upload_selector.uploading"}} {{uploadProgress}}% + {{#if isCancellable}} + {{d-icon "times"}} + {{/if}} +
diff --git a/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs b/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs index df794619..fa69a7cd 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs @@ -1,9 +1,21 @@ -

{{i18n "wizard_composer.insert_hyperlink.heading"}}

-{{input class="composer-link-name" placeholder=(i18n 'wizard_composer.insert_hyperlink.placeholder.link_name') type="text" value=linkName}} -{{input class="composer-link-url" placeholder=(i18n 'wizard_composer.insert_hyperlink.placeholder.link_url') type="text" value=linkUrl}} -{{d-button label="wizard_composer.insert_hyperlink.ok" - class="add-link" - click=(action 'addLink')}} -{{d-button label="wizard_composer.insert_hyperlink.cancel" - class="hide-hyperlink-box" - click=(action 'hideBox')}} + diff --git a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs b/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs index e5ffd904..b35e432b 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs @@ -1,10 +1,11 @@ -{{wizard-composer-editor replyPlaceholder=field.placeholder - composer=composer - groupsMentioned=(action "groupsMentioned") - cannotSeeMention=(action "cannotSeeMention") - importQuote=(action "importQuote") - togglePreview=(action "togglePreview") - afterRefresh=(action "afterRefresh")}} +{{wizard-composer-editor + replyPlaceholder=field.placeholder + composer=composer + groupsMentioned=(action "groupsMentioned") + cannotSeeMention=(action "cannotSeeMention") + importQuote=(action "importQuote") + togglePreview=(action "togglePreview") + afterRefresh=(action "afterRefresh")}}