0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-22 17:30:29 +01:00

Upload files, drag and drop files, syntax highlighting working

Dieser Commit ist enthalten in:
fzngagan 2020-10-01 11:13:29 +05:30
Ursprung 3fd3900cea
Commit 67d87f74c1
7 geänderte Dateien mit 271 neuen und 168 gelöschten Zeilen

Datei anzeigen

@ -38,6 +38,7 @@
//= require discourse/app/lib/cookie //= require discourse/app/lib/cookie
//= require discourse/app/lib/public-js-versions //= require discourse/app/lib/public-js-versions
//= require discourse/app/lib/load-oneboxes //= require discourse/app/lib/load-oneboxes
//= require discourse/app/lib/highlight-syntax
//= require discourse/app/mixins/singleton //= require discourse/app/mixins/singleton
//= require discourse/app/mixins/upload //= require discourse/app/mixins/upload
@ -128,6 +129,7 @@
//= require template_include.js //= require template_include.js
//= require caret_position.js //= require caret_position.js
//= require popper.js //= require popper.js
//= require bootstrap-modal.js
//= require bootbox.js //= require bootbox.js
//= require discourse-shims //= require discourse-shims

Datei anzeigen

@ -1,22 +1,26 @@
import ComposerEditor from 'discourse/components/composer-editor'; import ComposerEditor from "discourse/components/composer-editor";
import { default as computed, on } from 'discourse-common/utils/decorators'; import { default as computed, on } from "discourse-common/utils/decorators";
import { findRawTemplate } from "discourse-common/lib/raw-templates"; import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { throttle } from "@ember/runloop"; import { throttle } from "@ember/runloop";
import { scheduleOnce } from "@ember/runloop"; import { scheduleOnce } from "@ember/runloop";
import { safariHacksDisabled } from "discourse/lib/utilities"; 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({ export default ComposerEditor.extend({
classNameBindings: ['fieldClass'], classNameBindings: ["fieldClass"],
allowUpload: false, allowUpload: true,
showLink: false, showLink: false,
topic: null, topic: null,
showToolbar: true, showToolbar: true,
focusTarget: "reply", focusTarget: "reply",
canWhisper: false, canWhisper: false,
lastValidatedAt: 'lastValidatedAt', lastValidatedAt: "lastValidatedAt",
uploadIcon: "upload", uploadIcon: "upload",
popupMenuOptions: [], popupMenuOptions: [],
draftStatus: 'null', draftStatus: "null",
@on("didInsertElement") @on("didInsertElement")
_composerEditorInit() { _composerEditorInit() {
@ -26,12 +30,12 @@ export default ComposerEditor.extend({
if (this.siteSettings.enable_mentions) { if (this.siteSettings.enable_mentions) {
$input.autocomplete({ $input.autocomplete({
template: findRawTemplate("user-selector-autocomplete"), template: findRawTemplate("user-selector-autocomplete"),
dataSource: term => this.userSearchTerm.call(this, term), dataSource: (term) => this.userSearchTerm.call(this, term),
key: "@", key: "@",
transformComplete: v => v.username || v.name, transformComplete: (v) => v.username || v.name,
afterComplete() { afterComplete() {
scheduleOnce("afterRender", () => $input.blur().focus()); scheduleOnce("afterRender", () => $input.blur().focus());
} },
}); });
} }
@ -46,12 +50,72 @@ export default ComposerEditor.extend({
this._bindUploadTarget(); this._bindUploadTarget();
}, },
showUploadModal() {
$(".wizard-composer-upload").trigger("click");
},
_setUploadPlaceholderSend() {
if (!this.composer.get("reply")) {
this.composer.set("reply", "");
}
this._super(...arguments);
},
_bindUploadTarget() { _bindUploadTarget() {
}, this._super(...arguments);
const $element = $(this.element);
$element.off("fileuploadsubmit");
$element.on("fileuploadsubmit", (e, data) => {
const max = this.siteSettings.simultaneous_uploads;
_unbindUploadTarget() { // 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;
});
},
actions: { actions: {
extraButtons(toolbar) { extraButtons(toolbar) {
if (this.allowUpload && this.uploadIcon && !this.site.mobileView) { if (this.allowUpload && this.uploadIcon && !this.site.mobileView) {
@ -60,9 +124,13 @@ export default ComposerEditor.extend({
group: "insertions", group: "insertions",
icon: this.uploadIcon, icon: this.uploadIcon,
title: "upload", title: "upload",
sendAction: this.showUploadModal sendAction: this.showUploadModal,
}); });
} }
} },
} previewUpdated($preview) {
}) highlightSyntax($preview[0], this.siteSettings, this.session);
this._super(...arguments);
},
},
});

Datei anzeigen

@ -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"; import { dasherize } from "@ember/string";
export default { export default {
name: 'custom-routes', name: "custom-routes",
after: "sniff-capabilities",
initialize(app) { 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 EmberObject = requirejs("@ember/object").default;
const Router = requirejs('wizard/router').default; const Router = requirejs("wizard/router").default;
const ApplicationRoute = requirejs('wizard/routes/application').default; const ApplicationRoute = requirejs("wizard/routes/application").default;
const ajax = requirejs('wizard/lib/ajax').ajax; const ajax = requirejs("wizard/lib/ajax").ajax;
const StepModel = requirejs('wizard/models/step').default; const StepModel = requirejs("wizard/models/step").default;
const CustomWizard = requirejs('discourse/plugins/discourse-custom-wizard/wizard/models/custom').default; const CustomWizard = requirejs(
const WizardStep = requirejs('wizard/components/wizard-step').default; "discourse/plugins/discourse-custom-wizard/wizard/models/custom"
const WizardField = requirejs('wizard/components/wizard-field').default; ).default;
const getUrl = requirejs('discourse-common/lib/get-url').default; const WizardStep = requirejs("wizard/components/wizard-step").default;
const FieldModel = requirejs('wizard/models/wizard-field').default; const WizardField = requirejs("wizard/components/wizard-field").default;
const autocomplete = requirejs('discourse/lib/autocomplete').default; const getUrl = requirejs("discourse-common/lib/get-url").default;
const cook = requirejs('discourse/plugins/discourse-custom-wizard/wizard/lib/text-lite').cook; 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 Singleton = requirejs("discourse/mixins/singleton").default;
const Store = requirejs("discourse/models/store").default; const Store = requirejs("discourse/models/store").default;
const registerRawHelpers = requirejs("discourse-common/lib/raw-handlebars-helpers").registerRawHelpers; const registerRawHelpers = requirejs(
const createHelperContext = requirejs("discourse-common/lib/helpers").createHelperContext; "discourse-common/lib/raw-handlebars-helpers"
const RawHandlebars = requirejs("discourse-common/lib/raw-handlebars").default; ).registerRawHelpers;
const Site = requirejs("discourse/plugins/discourse-custom-wizard/wizard/models/site").default; 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 RestAdapter = requirejs("discourse/adapters/rest").default;
const Session = requirejs("discourse/models/session").default; const Session = requirejs("discourse/models/session").default;
const setDefaultOwner = requirejs("discourse-common/lib/get-owner").setDefaultOwner; const setDefaultOwner = requirejs("discourse-common/lib/get-owner")
const messageBus = requirejs('message-bus-client').default; .setDefaultOwner;
const messageBus = requirejs("message-bus-client").default;
const container = app.__container__; const container = app.__container__;
Discourse.Model = EmberObject.extend(); Discourse.Model = EmberObject.extend();
Discourse.__container__ = container; 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 // IE11 Polyfill - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries#Polyfill
if (!Object.entries) if (!Object.entries)
Object.entries = function( obj ){ Object.entries = function (obj) {
var ownProps = Object.keys( obj ), var ownProps = Object.keys(obj),
i = ownProps.length, i = ownProps.length,
resArray = new Array(i); // preallocate the Array resArray = new Array(i); // preallocate the Array
while (i--) while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]];
resArray[i] = [ownProps[i], obj[ownProps[i]]];
return resArray; return resArray;
}; };
$.fn.autocomplete = autocomplete; $.fn.autocomplete = autocomplete;
Object.keys(Ember.TEMPLATES).forEach(k => { Object.keys(Ember.TEMPLATES).forEach((k) => {
if (k.indexOf("select-kit") === 0) { if (k.indexOf("select-kit") === 0) {
let template = Ember.TEMPLATES[k]; let template = Ember.TEMPLATES[k];
define(k, () => template); define(k, () => template);
} }
@ -61,148 +71,162 @@ export default {
const siteSettings = Wizard.SiteSettings; const siteSettings = Wizard.SiteSettings;
app.register("site-settings:main", siteSettings, { instantiate: false }); app.register("site-settings:main", siteSettings, { instantiate: false });
createHelperContext({ siteSettings }); 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 }); 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); app.register("service:store", Store);
targets.forEach(t => app.inject(t, "store", "service: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, "appEvents", "service:app-events"));
app.register("adapter:rest", RestAdapter); app.register("adapter:rest", RestAdapter);
const site = Site.current(); const site = Site.current();
app.register("site:main", site, { instantiate: false }); app.register("site:main", site, { instantiate: false });
targets.forEach(t => app.inject(t, "site", "site:main")); targets.forEach((t) => app.inject(t, "site", "site:main"));
site.set('can_create_tag', false);
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 = { let context = {
siteSettings: container.lookup("site-settings:main"), siteSettings: container.lookup("site-settings:main"),
currentUser: container.lookup("current-user:main"), currentUser: container.lookup("current-user:main"),
site: container.lookup("site:main"), site: container.lookup("site:main"),
session: container.lookup("session:main"), session: container.lookup("session:main"),
capabilities: container.lookup("capabilities:main"),
}; };
createHelperContext(context); createHelperContext(context);
const session = container.lookup("session:main");
const setupData = document.getElementById("data-discourse-setup").dataset;
session.set("highlightJsPath", setupData.highlightJsPath);
Router.reopen({ Router.reopen({
rootURL: getUrl('/w/') rootURL: getUrl("/w/"),
}); });
Router.map(function() { Router.map(function () {
this.route('custom', { path: '/:wizard_id' }, function() { this.route("custom", { path: "/:wizard_id" }, function () {
this.route('steps'); this.route("steps");
this.route('step', { path: '/steps/:step_id' }); this.route("step", { path: "/steps/:step_id" });
}); });
}); });
ApplicationRoute.reopen({ ApplicationRoute.reopen({
redirect() { redirect() {
this.transitionTo('custom'); this.transitionTo("custom");
}, },
model() {} model() {},
}); });
WizardStep.reopen({ WizardStep.reopen({
classNameBindings: ['step.id'], classNameBindings: ["step.id"],
animateInvalidFields() { animateInvalidFields() {
Ember.run.scheduleOnce('afterRender', () => { Ember.run.scheduleOnce("afterRender", () => {
let $element = $('.invalid input[type=text], .invalid textarea, .invalid input[type=checkbox], .invalid .select-kit'); let $element = $(
".invalid input[type=text], .invalid textarea, .invalid input[type=checkbox], .invalid .select-kit"
);
if ($element.length) { if ($element.length) {
$([document.documentElement, document.body]).animate({ $([document.documentElement, document.body]).animate(
scrollTop: $element.offset().top - 200 {
}, 400, function() { scrollTop: $element.offset().top - 200,
$element.wiggle(2, 100); },
}); 400,
function () {
$element.wiggle(2, 100);
}
);
} }
}); });
}, },
ensureStartsAtTop: function() { ensureStartsAtTop: function () {
window.scrollTo(0,0); window.scrollTo(0, 0);
}.observes('step.id'), }.observes("step.id"),
showQuitButton: function() { showQuitButton: function () {
const index = this.get('step.index'); const index = this.get("step.index");
const required = this.get('wizard.required'); const required = this.get("wizard.required");
return index === 0 && !required; return index === 0 && !required;
}.property('step.index', 'wizard.required'), }.property("step.index", "wizard.required"),
cookedTitle: function() { cookedTitle: function () {
return cook(this.get('step.title')); return cook(this.get("step.title"));
}.property('step.title'), }.property("step.title"),
cookedDescription: function() { cookedDescription: function () {
return cook(this.get('step.description')); return cook(this.get("step.description"));
}.property('step.description'), }.property("step.description"),
bannerImage: function() { bannerImage: function () {
const src = this.get('step.banner'); const src = this.get("step.banner");
if (!src) return; if (!src) return;
return getUrl(src); return getUrl(src);
}.property('step.banner'), }.property("step.banner"),
handleMessage: function() { handleMessage: function () {
const message = this.get('step.message'); const message = this.get("step.message");
this.sendAction('showMessage', message); this.sendAction("showMessage", message);
}.observes('step.message'), }.observes("step.message"),
advance() { advance() {
this.set('saving', true); this.set("saving", true);
this.get('step').save() this.get("step")
.then(response => { .save()
if (this.get('finalStep')) { .then((response) => {
if (this.get("finalStep")) {
CustomWizard.finished(response); CustomWizard.finished(response);
} else { } else {
this.sendAction('goNext', response); this.sendAction("goNext", response);
} }
}) })
.catch(() => this.animateInvalidFields()) .catch(() => this.animateInvalidFields())
.finally(() => this.set('saving', false)); .finally(() => this.set("saving", false));
}, },
keyPress(key) { keyPress(key) {},
},
actions: { actions: {
quit() { quit() {
this.get('wizard').skip(); this.get("wizard").skip();
}, },
done() { done() {
this.set('finalStep', true); this.set("finalStep", true);
this.send('nextStep'); this.send("nextStep");
}, },
showMessage(message) { showMessage(message) {
this.sendAction('showMessage', message); this.sendAction("showMessage", message);
} },
} },
}); });
StepModel.reopen({ StepModel.reopen({
save() { save() {
const wizardId = this.get('wizardId'); const wizardId = this.get("wizardId");
const fields = {}; const fields = {};
this.get('fields').forEach(f => { this.get("fields").forEach((f) => {
if (f.type !== 'text_only') { if (f.type !== "text_only") {
fields[f.id] = f.value; fields[f.id] = f.value;
} }
}); });
return ajax({ return ajax({
url: `/w/${wizardId}/steps/${this.get('id')}`, url: `/w/${wizardId}/steps/${this.get("id")}`,
type: 'PUT', type: "PUT",
data: { fields } data: { fields },
}).catch(response => { }).catch((response) => {
if (response && response.responseJSON && response.responseJSON.errors) { if (
response &&
response.responseJSON &&
response.responseJSON.errors
) {
let wizardErrors = []; let wizardErrors = [];
response.responseJSON.errors.forEach(err => { response.responseJSON.errors.forEach((err) => {
if (err.field === wizardId) { if (err.field === wizardId) {
wizardErrors.push(err.description); wizardErrors.push(err.description);
} else if (err.field) { } else if (err.field) {
@ -212,7 +236,7 @@ export default {
} }
}); });
if (wizardErrors.length) { if (wizardErrors.length) {
this.handleWizardError(wizardErrors.join('\n')); this.handleWizardError(wizardErrors.join("\n"));
} }
this.animateInvalidFields(); this.animateInvalidFields();
throw response; throw response;
@ -220,8 +244,8 @@ export default {
if (response && response.responseText) { if (response && response.responseText) {
const responseText = response.responseText; const responseText = response.responseText;
const start = responseText.indexOf('>') + 1; const start = responseText.indexOf(">") + 1;
const end = responseText.indexOf('plugins'); const end = responseText.indexOf("plugins");
const message = responseText.substring(start, end); const message = responseText.substring(start, end);
this.handleWizardError(message); this.handleWizardError(message);
throw message; throw message;
@ -230,44 +254,44 @@ export default {
}, },
handleWizardError(message) { handleWizardError(message) {
this.set('message', { this.set("message", {
state: 'error', state: "error",
text: message text: message,
}); });
Ember.run.later(() => this.set('message', null), 6000); Ember.run.later(() => this.set("message", null), 6000);
} },
}); });
WizardField.reopen({ WizardField.reopen({
classNameBindings: ['field.id'], classNameBindings: ["field.id"],
cookedDescription: function() { cookedDescription: function () {
return cook(this.get('field.description')); return cook(this.get("field.description"));
}.property('field.description'), }.property("field.description"),
inputComponentName: function() { inputComponentName: function () {
const type = this.get('field.type'); const type = this.get("field.type");
const id = this.get('field.id'); const id = this.get("field.id");
if (['text_only'].includes(type)) return false; if (["text_only"].includes(type)) return false;
return dasherize((type === 'component') ? id : `wizard-field-${type}`); return dasherize(type === "component" ? id : `wizard-field-${type}`);
}.property('field.type', 'field.id') }.property("field.type", "field.id"),
}); });
const StandardFieldValidation = [ const StandardFieldValidation = [
'text', "text",
'number', "number",
'textarea', "textarea",
'dropdown', "dropdown",
'tag', "tag",
'image', "image",
'user_selector', "user_selector",
'text_only', "text_only",
'composer', "composer",
'category', "category",
'group', "group",
'date', "date",
'time', "time",
'date_time' "date_time",
]; ];
FieldModel.reopen({ FieldModel.reopen({
@ -283,23 +307,23 @@ export default {
return true; return true;
} }
const val = this.get('value'); const val = this.get("value");
const type = this.get('type'); const type = this.get("type");
if (type === 'checkbox') { if (type === "checkbox") {
valid = val; valid = val;
} else if (type === 'upload') { } else if (type === "upload") {
valid = val && val.id > 0; valid = val && val.id > 0;
} else if (StandardFieldValidation.indexOf(type) > -1) { } else if (StandardFieldValidation.indexOf(type) > -1) {
valid = val && val.toString().length > 0; valid = val && val.toString().length > 0;
} else if (type === 'url') { } else if (type === "url") {
valid = true; valid = true;
} }
this.setValid(valid); this.setValid(valid);
return valid; return valid;
} },
}); });
} },
}; };

Datei anzeigen

@ -14,3 +14,5 @@
composerEvents=true composerEvents=true
disabled=disableTextarea disabled=disableTextarea
outletArgs=(hash composer=composer editorType="composer")}} outletArgs=(hash composer=composer editorType="composer")}}
{{input class="wizard-composer-upload hidden-upload-field" disabled=uploading type="file" multiple=true }}

Datei anzeigen

@ -183,3 +183,7 @@
.d-editor-button-bar .btn { .d-editor-button-bar .btn {
border: none; border: none;
} }
.wizard-composer-upload {
display: none;
}

Datei anzeigen

@ -1,5 +1,8 @@
@import "common/foundation/colors"; @import "common/foundation/colors";
@import "common/foundation/variables"; @import "common/foundation/variables";
@import "common/base/code_highlighting";
@import "common/base/modal";
@import "desktop/modal";
@import "custom/base"; @import "custom/base";
@import "custom/wizard"; @import "custom/wizard";
@import "custom/step"; @import "custom/step";
@ -9,5 +12,3 @@
@import "custom/composer"; @import "custom/composer";
@import "custom/events"; @import "custom/events";
@import "custom/locations"; @import "custom/locations";

Datei anzeigen

@ -23,6 +23,8 @@
<%= server_plugin_outlet "custom_wizard" %> <%= server_plugin_outlet "custom_wizard" %>
<%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %>
<meta name="discourse_theme_ids" content="<%= theme_ids&.join(",") %>"> <meta name="discourse_theme_ids" content="<%= theme_ids&.join(",") %>">
<meta name="discourse-base-uri" content="<%= Discourse.base_uri %>"> <meta name="discourse-base-uri" content="<%= Discourse.base_uri %>">