diff --git a/assets/javascripts/wizard-custom-lib.js b/assets/javascripts/wizard-custom-globals.js similarity index 68% rename from assets/javascripts/wizard-custom-lib.js rename to assets/javascripts/wizard-custom-globals.js index 9767141d..e37648e5 100644 --- a/assets/javascripts/wizard-custom-lib.js +++ b/assets/javascripts/wizard-custom-globals.js @@ -1,7 +1,5 @@ window.Discourse = {} window.Wizard = {}; Wizard.SiteSettings = {}; -Wizard.RAW_TEMPLATES = {}; Discourse.__widget_helpers = {}; -window.__DISCOURSE_RAW_TEMPLATES = {}; Discourse.SiteSettings = Wizard.SiteSettings; \ No newline at end of file diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index 5840393c..c9a8c915 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -38,11 +38,15 @@ //= 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 //= 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 @@ -77,6 +81,7 @@ //= require discourse/app/helpers/user-avatar //= require discourse/app/helpers/format-username //= require discourse/app/helpers/share-url +//= require discourse/app/helpers/decorate-username-selector //= require discourse-common/addon/helpers/component-for-collection //= require discourse-common/addon/helpers/component-for-row //= require discourse-common/addon/lib/raw-templates @@ -113,6 +118,7 @@ //= require discourse/app/templates/emoji-selector-autocomplete //= require discourse/app/templates/user-selector-autocomplete +//= require discourse/app/initializers/jquery-plugins //= require discourse/app/pre-initializers/sniff-capabilities //= require ember-addons/decorator-alias @@ -126,6 +132,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-raw-templates.js.erb b/assets/javascripts/wizard-raw-templates.js.erb index b6a5b7f7..75a44abc 100644 --- a/assets/javascripts/wizard-raw-templates.js.erb +++ b/assets/javascripts/wizard-raw-templates.js.erb @@ -8,24 +8,30 @@ Discourse.unofficial_plugins.each do |plugin| files = [] plugin.each_globbed_asset do |f, is_dir| - files.push(f) if f.include? "raw.hbs" + files.push(f) if f.include? "hbr" end - Dir.glob("#{Rails.root}/app/assets/javascripts/discourse/templates/*.raw.hbs").each do |f| + Dir.glob("#{Rails.root}/app/assets/javascripts/discourse/app/templates/*.hbr").each do |f| files.push(f) end files.each do |f| - name = File.basename(f, ".raw.hbs") + name = File.basename(f, ".hbr") compiled = Barber::Precompiler.new().compile(File.read(f)) result << " (function() { - if ('Wizard' in window) { - window.__DISCOURSE_RAW_TEMPLATES['javascripts/#{name}'] = requirejs('discourse-common/lib/raw-handlebars').template(#{compiled}); - } + const { addRawTemplate } = requirejs('discourse-common/lib/raw-templates'); + addRawTemplate(#{compiled}); })(); " end + + result << " + (function() { + const rawTemplates = requirejs('discourse-common/lib/raw-templates'); + window.__DISCOURSE_RAW_TEMPLATES = rawTemplates.__DISCOURSE_RAW_TEMPLATES; + })(); + " end end result diff --git a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 index 50061883..e74cbc77 100644 --- a/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 +++ b/assets/javascripts/wizard/components/wizard-composer-editor.js.es6 @@ -1,37 +1,57 @@ -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 { scheduleOnce, next } from "@ember/runloop"; +import { + safariHacksDisabled, + caretPosition, + inCodeBlock, +} from "discourse/lib/utilities"; +import highlightSyntax from "discourse/lib/highlight-syntax"; +import { getToken } from "wizard/lib/ajax"; +import { + validateUploadedFiles, + getUploadMarkdown +} from "discourse/lib/uploads"; +import { + cacheShortUploadUrl, +} from "pretty-text/upload-short-url"; +const uploadMarkdownResolvers = []; + +const uploadHandlers = []; export default ComposerEditor.extend({ - classNameBindings: ['fieldClass'], - allowUpload: false, + classNameBindings: ["fieldClass"], + allowUpload: true, showLink: false, + showHyperlinkBox: 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, - afterComplete() { + transformComplete: (v) => v.username || v.name, + afterComplete: (value) => { + this.composer.set("reply", value); scheduleOnce("afterRender", () => $input.blur().focus()); - } + }, + triggerRule: (textarea) => + !inCodeBlock(textarea.value, caretPosition(textarea)) }); } @@ -45,24 +65,248 @@ export default ComposerEditor.extend({ this._bindUploadTarget(); }, - + + _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; + }); + + $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); + } + }); + + $element.on("fileuploadsend", (e, data) => { + this._pasted = false; + this._validUploads++; + + this._setUploadPlaceholderSend(data); + + this.appEvents.trigger("wizard-editor:insert-text", { + fieldId: this.fieldId, + text: this.uploadPlaceholder + }); + + if (data.xhr && data.originalFiles.length === 1) { + this.set("isCancellable", true); + this._xhr = data.xhr(); + } + }); + + $element.on("fileuploaddone", (e, data) => { + let upload = data.result; + + this._setUploadPlaceholderDone(data); + + if (!this._xhr || !this._xhr._userCancelled) { + const markdown = uploadMarkdownResolvers.reduce( + (md, resolver) => resolver(upload) || md, + getUploadMarkdown(upload) + ); + + cacheShortUploadUrl(upload.short_url, upload); + this.appEvents.trigger( + "wizard-editor:replace-text", { + fieldId: this.fieldId, + oldVal: this.uploadPlaceholder.trim(), + newVal: markdown + } + ); + this._resetUpload(false); + } else { + this._resetUpload(true); + } + }); }, - _unbindUploadTarget() { + _resetUpload(removePlaceholder) { + next(() => { + if (this._validUploads > 0) { + this._validUploads--; + } + if (this._validUploads === 0) { + this.setProperties({ + uploadProgress: 0, + isUploading: false, + isCancellable: false, + }); + } + if (removePlaceholder) { + this.appEvents.trigger( + "wizard-editor:replace-text", { + fieldId: this.fieldId, + oldVal: this.uploadPlaceholder, + newVal: "" + } + ); + } + this._resetUploadFilenamePlaceholder(); + }); + }, + + _registerImageScaleButtonClick($preview) { + const imageScaleRegex = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?(.*?)\]\((upload:\/\/.*?)\)(?!(.*`))/g; + $preview.off("click", ".scale-btn").on("click", ".scale-btn", (e) => { + const index = parseInt($(e.target).parent().attr("data-image-index"), 10); + + const scale = e.target.attributes["data-scale"].value; + const matchingPlaceholder = this.get("composer.reply").match( + imageScaleRegex + ); + + if (matchingPlaceholder) { + const match = matchingPlaceholder[index]; + + if (match) { + const replacement = match.replace( + imageScaleRegex, + `![$1|$2, ${scale}%$4]($5)` + ); + + this.appEvents.trigger( + "wizard-editor:replace-text", + { + fieldId: this.fieldId, + oldVal: matchingPlaceholder[index], + newVal: replacement, + options: { + regex: imageScaleRegex, + index + } + } + ); + } + } + + e.preventDefault(); + return; + }); + }, + + click(e) { + if ($(e.target).hasClass('wizard-composer-hyperlink')) { + this.set('showHyperlinkBox', false); + } }, actions: { extraButtons(toolbar) { + const component = this; + if (this.allowUpload && this.uploadIcon && !this.site.mobileView) { toolbar.addButton({ id: "upload", group: "insertions", icon: this.uploadIcon, title: "upload", - sendAction: this.showUploadModal + sendAction: (event) => component.send("showUploadModal", event), }); } + + toolbar.addButton({ + id: "link", + 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("wizard-editor:insert-text", { + fieldId: this.fieldId, + text: link + }); + this.set("showHyperlinkBox", false); + }, + + hideBox() { + this.set("showHyperlinkBox", false); + }, + + showUploadModal() { + $(this.element.querySelector(".wizard-composer-upload")).trigger("click"); } - } -}) \ No newline at end of file + }, +}); 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..5cc0357d --- /dev/null +++ b/assets/javascripts/wizard/components/wizard-composer-hyperlink.js.es6 @@ -0,0 +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/custom-wizard.js.es6 b/assets/javascripts/wizard/custom-wizard.js.es6 index 457c100b..969e969e 100644 --- a/assets/javascripts/wizard/custom-wizard.js.es6 +++ b/assets/javascripts/wizard/custom-wizard.js.es6 @@ -5,16 +5,6 @@ export default Ember.Application.extend({ Resolver: buildResolver("wizard"), start() { - Object.keys(requirejs._eak_seen).forEach(key => { - if (/\/initializers\//.test(key)) { - const module = requirejs(key, null, null, true); - if (!module) { - throw new Error(key + " must export an initializer."); - } - this.initializer(module.default); - } - }); - Object.keys(requirejs._eak_seen).forEach((key) => { if (/\/pre\-initializers\//.test(key)) { const module = requirejs(key, null, null, true); @@ -31,5 +21,15 @@ export default Ember.Application.extend({ this.initializer(init); } }); + + Object.keys(requirejs._eak_seen).forEach(key => { + if (/\/initializers\//.test(key)) { + const module = requirejs(key, null, null, true); + if (!module) { + throw new Error(key + " must export an initializer."); + } + this.initializer(module.default); + } + }); } }); diff --git a/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 b/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 new file mode 100644 index 00000000..75327413 --- /dev/null +++ b/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 @@ -0,0 +1,178 @@ +import { dasherize } from "@ember/string"; + +export default { + name: "custom-wizard-field", + initialize(app) { + if (window.location.pathname.indexOf("/w/") < 0) return; + + const FieldComponent = requirejs("wizard/components/wizard-field").default; + const FieldModel = requirejs("wizard/models/wizard-field").default; + const { cook } = requirejs("discourse/plugins/discourse-custom-wizard/wizard/lib/text-lite"); + const DEditor = requirejs("discourse/components/d-editor").default; + const { clipboardHelpers } = requirejs("discourse/lib/utilities"); + const { toMarkdown } = requirejs("discourse/lib/to-markdown"); + + FieldComponent.reopen({ + classNameBindings: ["field.id"], + + 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"), + }); + + const StandardFieldValidation = [ + "text", + "number", + "textarea", + "dropdown", + "tag", + "image", + "user_selector", + "text_only", + "composer", + "category", + "group", + "date", + "time", + "date_time", + ]; + + FieldModel.reopen({ + check() { + if (this.customCheck) { + return this.customCheck(); + } + + let valid = this.valid; + + if (!this.required) { + this.setValid(true); + return true; + } + + const val = this.get("value"); + const type = this.get("type"); + + if (type === "checkbox") { + valid = val; + } 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") { + valid = true; + } + + this.setValid(valid); + + return valid; + }, + }); + + const isInside = (text, regex) => { + const matches = text.match(regex); + return matches && matches.length % 2; + }; + + DEditor.reopen({ + isComposer: true, + + didInsertElement() { + this._super(); + if (this.wizardComposerEvents) { + this.appEvents.on("wizard-editor:insert-text", this, "_wizardInsertText"); + this.appEvents.on("wizard-editor:replace-text", this, "_wizardReplaceText"); + } + }, + + _wizardInsertText(args = {}) { + if (args.fieldId === this.fieldId) { + this._insertText(args.text, args.options); + } + }, + + _wizardReplaceText(args = {}) { + if (args.fieldId === this.fieldId) { + this._replaceText(args.oldVal, args.newVal, args.opts = {}); + } + }, + + paste(e) { + if (!$(".d-editor-input").is(":focus")) { + return; + } + + const isComposer = this.isComposer; + let { clipboard, canPasteHtml, canUpload } = clipboardHelpers(e, { + siteSettings: this.siteSettings, + canUpload: isComposer, + }); + + let plainText = clipboard.getData("text/plain"); + let html = clipboard.getData("text/html"); + let handled = false; + + const { pre, lineVal } = this._getSelected(null, { lineVal: true }); + const isInlinePasting = pre.match(/[^\n]$/); + const isCodeBlock = isInside(pre, /(^|\n)```/g); + + if ( + plainText && + this.siteSettings.enable_rich_text_paste && + !isInlinePasting && + !isCodeBlock + ) { + plainText = plainText.trim().replace(/\r/g, ""); + const table = this._extractTable(plainText); + if (table) { + this.appEvents.trigger("wizard-editor:insert-text", { + fieldId: this.fieldId, + text: table + }); + handled = true; + } + } + + if (canPasteHtml && plainText) { + if (isInlinePasting) { + canPasteHtml = !( + lineVal.match(/^```/) || + isInside(pre, /`/g) || + lineVal.match(/^ /) + ); + } else { + canPasteHtml = !isCodeBlock; + } + } + + if (canPasteHtml && !handled) { + let markdown = toMarkdown(html); + + if (!plainText || plainText.length < markdown.length) { + if (isInlinePasting) { + markdown = markdown.replace(/^#+/, "").trim(); + markdown = pre.match(/\S$/) ? ` ${markdown}` : markdown; + } + + this.appEvents.trigger("composer:insert-text", { + fieldId: this.fieldId, + text: markdown + }); + handled = true; + } + } + + if (handled || (canUpload && !plainText)) { + e.preventDefault(); + } + }, + }); + } +} \ No newline at end of file diff --git a/assets/javascripts/wizard/initializers/custom-wizard-step.js.es6 b/assets/javascripts/wizard/initializers/custom-wizard-step.js.es6 new file mode 100644 index 00000000..c8393c1a --- /dev/null +++ b/assets/javascripts/wizard/initializers/custom-wizard-step.js.es6 @@ -0,0 +1,153 @@ + +export default { + name: "custom-wizard-step", + initialize(app) { + if (window.location.pathname.indexOf("/w/") < 0) return; + + const StepModel = requirejs("wizard/models/step").default; + const StepComponent = requirejs("wizard/components/wizard-step").default; + const ajax = requirejs("wizard/lib/ajax").ajax; + const cook = requirejs("discourse/plugins/discourse-custom-wizard/wizard/lib/text-lite").cook; + + StepModel.reopen({ + save() { + const wizardId = this.get("wizardId"); + const fields = {}; + + 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 + ) { + let wizardErrors = []; + response.responseJSON.errors.forEach((err) => { + if (err.field === wizardId) { + wizardErrors.push(err.description); + } else if (err.field) { + this.fieldError(err.field, err.description); + } else if (err) { + wizardErrors.push(err); + } + }); + if (wizardErrors.length) { + this.handleWizardError(wizardErrors.join("\n")); + } + this.animateInvalidFields(); + throw response; + } + + if (response && response.responseText) { + const responseText = response.responseText; + const start = responseText.indexOf(">") + 1; + const end = responseText.indexOf("plugins"); + const message = responseText.substring(start, end); + this.handleWizardError(message); + throw message; + } + }); + }, + + handleWizardError(message) { + this.set("message", { + state: "error", + text: message, + }); + Ember.run.later(() => this.set("message", null), 6000); + }, + }); + + StepComponent.reopen({ + classNameBindings: ["step.id"], + + animateInvalidFields() { + 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); + } + ); + } + }); + }, + + ensureStartsAtTop: function () { + window.scrollTo(0, 0); + }.observes("step.id"), + + showQuitButton: function () { + const index = this.get("step.index"); + const required = this.get("wizard.required"); + return index === 0 && !required; + }.property("step.index", "wizard.required"), + + cookedTitle: function () { + return cook(this.get("step.title")); + }.property("step.title"), + + cookedDescription: function () { + return cook(this.get("step.description")); + }.property("step.description"), + + bannerImage: function () { + const src = this.get("step.banner"); + if (!src) return; + return getUrl(src); + }.property("step.banner"), + + 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")) { + CustomWizard.finished(response); + } else { + this.sendAction("goNext", response); + } + }) + .catch(() => this.animateInvalidFields()) + .finally(() => this.set("saving", false)); + }, + + keyPress(key) {}, + + actions: { + quit() { + this.get("wizard").skip(); + }, + + done() { + this.set("finalStep", true); + this.send("nextStep"); + }, + + showMessage(message) { + this.sendAction("showMessage", message); + }, + }, + }); + } +} \ No newline at end of file diff --git a/assets/javascripts/wizard/initializers/custom-wizard.js.es6 b/assets/javascripts/wizard/initializers/custom-wizard.js.es6 new file mode 100644 index 00000000..31f25c9e --- /dev/null +++ b/assets/javascripts/wizard/initializers/custom-wizard.js.es6 @@ -0,0 +1,100 @@ +export default { + name: "custom-routes", + initialize(app) { + 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 CustomWizard = requirejs("discourse/plugins/discourse-custom-wizard/wizard/models/custom").default; + const getUrl = requirejs("discourse-common/lib/get-url").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 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; + setDefaultOwner(container); + registerRawHelpers(RawHandlebars, Handlebars); + + // 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]]]; + + return resArray; + }; + } + + Object.keys(Ember.TEMPLATES).forEach((k) => { + if (k.indexOf("select-kit") === 0) { + let template = Ember.TEMPLATES[k]; + define(k, () => template); + } + }); + + const targets = ["controller", "component", "route", "model", "adapter"]; + + const siteSettings = Wizard.SiteSettings; + 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")); + + 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); + app.register("session:main", Session.current(), { instantiate: false }); + targets.forEach((t) => app.inject(t, "session", "session:main")); + + createHelperContext({ + 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"), + }); + + const session = container.lookup("session:main"); + const setupData = document.getElementById("data-discourse-setup").dataset; + session.set("highlightJsPath", setupData.highlightJsPath); + + Router.reopen({ + rootURL: getUrl("/w/") + }); + + 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"); + }, + model() {}, + }); + }, +}; diff --git a/assets/javascripts/wizard/initializers/custom.js.es6 b/assets/javascripts/wizard/initializers/custom.js.es6 deleted file mode 100644 index 89bcf91c..00000000 --- a/assets/javascripts/wizard/initializers/custom.js.es6 +++ /dev/null @@ -1,302 +0,0 @@ -import { default as computed } from 'discourse-common/utils/decorators'; -import { dasherize } from "@ember/string"; - -export default { - name: 'custom-routes', - - initialize(app) { - 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 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 RestAdapter = requirejs("discourse/adapters/rest").default; - const Session = requirejs("discourse/models/session").default; - const setDefaultOwner = requirejs("discourse-common/lib/get-owner").setDefaultOwner; - - const container = app.__container__; - Discourse.Model = EmberObject.extend(); - Discourse.__container__ = container; - setDefaultOwner(container); - registerRawHelpers(RawHandlebars, Handlebars); - - // 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]]]; - - return resArray; - }; - - $.fn.autocomplete = autocomplete; - - Object.keys(Ember.TEMPLATES).forEach(k => { - if (k.indexOf("select-kit") === 0) { - let template = Ember.TEMPLATES[k]; - define(k, () => template); - } - }); - - const targets = ["controller", "component", "route", "model", "adapter"]; - - const siteSettings = Wizard.SiteSettings; - app.register("site-settings:main", siteSettings, { instantiate: false }); - createHelperContext({ siteSettings }); - targets.forEach(t => app.inject(t, "siteSettings", "site-settings: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")); - - 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); - - let context = { - siteSettings: container.lookup("site-settings:main"), - currentUser: container.lookup("current-user:main"), - site: container.lookup("site:main"), - session: container.lookup("session:main"), - }; - createHelperContext(context); - - Router.reopen({ - rootURL: getUrl('/w/') - }); - - 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'); - }, - - model() {} - }); - - WizardStep.reopen({ - classNameBindings: ['step.id'], - - animateInvalidFields() { - 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); - }); - } - }); - }, - - ensureStartsAtTop: function() { - window.scrollTo(0,0); - }.observes('step.id'), - - showQuitButton: function() { - const index = this.get('step.index'); - const required = this.get('wizard.required'); - return index === 0 && !required; - }.property('step.index', 'wizard.required'), - - cookedTitle: function() { - return cook(this.get('step.title')); - }.property('step.title'), - - cookedDescription: function() { - return cook(this.get('step.description')); - }.property('step.description'), - - bannerImage: function() { - const src = this.get('step.banner'); - if (!src) return; - return getUrl(src); - }.property('step.banner'), - - 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')) { - CustomWizard.finished(response); - } else { - this.sendAction('goNext', response); - } - }) - .catch(() => this.animateInvalidFields()) - .finally(() => this.set('saving', false)); - }, - - keyPress(key) { - }, - - actions: { - quit() { - this.get('wizard').skip(); - }, - - done() { - this.set('finalStep', true); - this.send('nextStep'); - }, - - showMessage(message) { - this.sendAction('showMessage', message); - } - } - }); - - StepModel.reopen({ - save() { - const wizardId = this.get('wizardId'); - const fields = {}; - - 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) { - let wizardErrors = []; - response.responseJSON.errors.forEach(err => { - if (err.field === wizardId) { - wizardErrors.push(err.description); - } else if (err.field) { - this.fieldError(err.field, err.description); - } else if (err) { - wizardErrors.push(err); - } - }); - if (wizardErrors.length) { - this.handleWizardError(wizardErrors.join('\n')); - } - this.animateInvalidFields(); - throw response; - } - - if (response && response.responseText) { - const responseText = response.responseText; - const start = responseText.indexOf('>') + 1; - const end = responseText.indexOf('plugins'); - const message = responseText.substring(start, end); - this.handleWizardError(message); - throw message; - } - }); - }, - - handleWizardError(message) { - this.set('message', { - state: 'error', - text: message - }); - Ember.run.later(() => this.set('message', null), 6000); - } - }); - - WizardField.reopen({ - classNameBindings: ['field.id'], - - 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') - }); - - const StandardFieldValidation = [ - 'text', - 'number', - 'textarea', - 'dropdown', - 'tag', - 'image', - 'user_selector', - 'text_only', - 'composer', - 'category', - 'group', - 'date', - 'time', - 'date_time' - ]; - - FieldModel.reopen({ - check() { - if (this.customCheck) { - return this.customCheck(); - } - - let valid = this.valid; - - if (!this.required) { - this.setValid(true); - return true; - } - - const val = this.get('value'); - const type = this.get('type'); - - if (type === 'checkbox') { - valid = val; - } 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') { - 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..068e6a56 100644 --- a/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-composer-editor.hbs @@ -6,11 +6,33 @@ markdownOptions=markdownOptions extraButtons=(action "extraButtons") importQuote=(action "importQuote") - showUploadModal=showUploadModal + showUploadModal=(action "showUploadModal") togglePreview=(action "togglePreview") validation=validation loading=composer.loading showLink=showLink - composerEvents=true + wizardComposerEvents=true + fieldId=fieldId 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=isUploading + type="file" + multiple=true}} + +{{#if showHyperlinkBox}} + {{wizard-composer-hyperlink + addLink=(action 'addLink') + hideBox=(action 'hideBox')}} +{{/if}} + +{{#if isUploading}} +
+ {{loading-spinner size="small"}}{{i18n "upload_selector.uploading"}} {{uploadProgress}}% + {{#if isCancellable}} + {{d-icon "times"}} + {{/if}} +
+{{/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..fa69a7cd --- /dev/null +++ b/assets/javascripts/wizard/templates/components/wizard-composer-hyperlink.hbs @@ -0,0 +1,21 @@ + diff --git a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs b/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs index e5ffd904..d2306616 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-composer.hbs @@ -1,10 +1,12 @@ -{{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 + fieldId=field.id + composer=composer + groupsMentioned=(action "groupsMentioned") + cannotSeeMention=(action "cannotSeeMention") + importQuote=(action "importQuote") + togglePreview=(action "togglePreview") + afterRefresh=(action "afterRefresh")}}