1) {
- // base tag available (Plone 4)
- return reg[1];
- }
- return "";
- },
- },
- form: function (actions) {
- var self = this;
- var $modal = self.$modal;
-
- if (self.options.automaticallyAddButtonActions) {
- actions[self.options.buttons] = {};
- }
- actions.a = {};
-
- $.each(actions, function (action, options) {
- var actionKeys = _.union(_.keys(self.options.actionOptions), [
- "templateOptions",
- ]);
- var actionOptions = $.extend(
- true,
- {},
- self.options.actionOptions,
- _.pick(options, actionKeys)
- );
- options.templateOptions = $.extend(
- true,
- options.templateOptions,
- self.options.templateOptions
- );
-
- var patternKeys = _.union(_.keys(self.options.actionOptions), [
- "actions",
- "actionOptions",
- ]);
- var patternOptions = $.extend(
- true,
- _.omit(options, patternKeys),
- self.options
- );
- $(action, $("." + options.templateOptions.classBodyName, $modal)).each(
- function () {
- var $action = $(this);
- $action.on(actionOptions.eventType, function (e) {
- e.stopPropagation();
- e.preventDefault();
-
- self.loading.show(false);
-
- // handle event on $action using a function on self
- if (actionOptions.modalFunction !== null) {
- self[actionOptions.modalFunction]();
- // handle event on input/button using jquery.form library
- } else if (
- $.nodeName($action[0], "input") ||
- $.nodeName($action[0], "button") ||
- options.isForm === true
- ) {
- self.options.handleFormAction.apply(self, [
- $action,
- actionOptions,
- patternOptions,
- ]);
- // handle event on link with jQuery.ajax
- } else if (
- options.ajaxUrl !== null ||
- $.nodeName($action[0], "a")
- ) {
- self.options.handleLinkAction.apply(self, [
- $action,
- actionOptions,
- patternOptions,
- ]);
- }
- });
- }
- );
- });
- },
- handleFormAction: function ($action, options, patternOptions) {
- var self = this;
-
- // pass action that was clicked when submiting form
- var extraData = {};
- extraData[$action.attr("name")] = $action.attr("value");
-
- var $form;
-
- if ($.nodeName($action[0], "form")) {
- $form = $action;
- } else {
- $form = $action.parents("form:not(.disableAutoSubmit)");
- }
-
- var url;
- if (options.ajaxUrl !== null) {
- if (typeof options.ajaxUrl === "function") {
- url = options.ajaxUrl.apply(self, [$action, options]);
- } else {
- url = options.ajaxUrl;
- }
- } else {
- url = $action.parents("form").attr("action");
- }
-
- if (options.disableAjaxFormSubmit) {
- if ($action.attr("name") && $action.attr("value")) {
- $form.append(
- $(
- ''
- )
- );
- }
- $form.trigger("submit");
- return;
- }
- // We want to trigger the form submit event but NOT use the default
- $form.on("submit", function (e) {
- e.preventDefault();
- });
- $form.trigger("submit");
-
- self.loading.show(false);
- $form.ajaxSubmit({
- timeout: options.timeout,
- data: extraData,
- url: url,
- error: function (xhr, textStatus, errorStatus) {
- self.loading.hide();
- if (textStatus === "timeout" && options.onTimeout) {
- options.onTimeout.apply(self, xhr, errorStatus);
- // on "error", "abort", and "parsererror"
- } else if (options.onError) {
- if (typeof options.onError === "string") {
- window[options.onError](xhr, textStatus, errorStatus);
- } else {
- options.onError(xhr, textStatus, errorStatus);
- }
- } else {
- // window.alert(_t('There was an error submitting the form.'));
- console.log("error happened", textStatus, " do something");
- }
- self.emit("formActionError", [xhr, textStatus, errorStatus]);
- },
- success: function (response, state, xhr, form) {
- self.loading.hide();
- // if error is found (NOTE: check for both the portal errors
- // and the form field-level errors)
- if (
- $(options.error, response).length !== 0 ||
- $(options.formFieldError, response).length !== 0
- ) {
- if (options.onFormError) {
- if (typeof options.onFormError === "string") {
- window[options.onFormError](
- self,
- response,
- state,
- xhr,
- form
- );
- } else {
- options.onFormError(self, response, state, xhr, form);
- }
- } else {
- self.redraw(response, patternOptions);
- }
- return;
- }
-
- if (options.redirectOnResponse === true) {
- if (typeof options.redirectToUrl === "function") {
- window.parent.location.href = options.redirectToUrl.apply(
- self,
- [$action, response, options]
- );
- } else {
- window.parent.location.href = options.redirectToUrl;
- }
- return; // cut out right here since we're changing url
- }
-
- if (options.onSuccess) {
- if (typeof options.onSuccess === "string") {
- window[options.onSuccess](self, response, state, xhr, form);
- } else {
- options.onSuccess(self, response, state, xhr, form);
- }
- }
-
- if (options.displayInModal === true) {
- self.redraw(response, patternOptions);
- } else {
- $action.trigger("destroy.plone-modal.patterns");
- // also calls hide
- if (options.reloadWindowOnClose) {
- self.reloadWindow();
- }
- }
- self.emit("formActionSuccess", [response, state, xhr, form]);
- },
- });
- },
- handleLinkAction: function ($action, options, patternOptions) {
- var self = this;
- var url;
- if ($action.hasClass("pat-plone-modal")) {
- // if link is a modal pattern, do not reload the page
- return;
- }
-
- // Figure out URL
- if (options.ajaxUrl) {
- if (typeof options.ajaxUrl === "function") {
- url = options.ajaxUrl.apply(self, [$action, options]);
- } else {
- url = options.ajaxUrl;
- }
- } else {
- url = $action.attr("href");
- }
-
- // Non-ajax link (I know it says "ajaxUrl" ...)
- if (options.displayInModal === false) {
- if ($action.attr("target") === "_blank") {
- window.open(url, "_blank");
- self.loading.hide();
- } else {
- window.location = url;
- }
- return;
- }
-
- // ajax version
- $.ajax({
- url: url,
- })
- .fail(function (xhr, textStatus, errorStatus) {
- if (textStatus === "timeout" && options.onTimeout) {
- options.onTimeout(self.$modal, xhr, errorStatus);
-
- // on "error", "abort", and "parsererror"
- } else if (options.onError) {
- options.onError(xhr, textStatus, errorStatus);
- } else {
- window.alert(_t("There was an error loading modal."));
- }
- self.emit("linkActionError", [xhr, textStatus, errorStatus]);
- })
- .done(function (response, state, xhr) {
- self.redraw(response, patternOptions);
- if (options.onSuccess) {
- if (typeof options.onSuccess === "string") {
- window[options.onSuccess](self, response, state, xhr);
- } else {
- options.onSuccess(self, response, state, xhr);
- }
- }
-
- self.emit("linkActionSuccess", [response, state, xhr]);
- })
- .always(function () {
- self.loading.hide();
- });
- },
- render: function (options) {
- var self = this;
-
- self.emit("before-render");
-
- if (!self.$raw) {
- return;
- }
- var $raw = self.$raw.clone();
-
- // Object that will be passed to the template
- var tplObject = {
- title: "",
- prepend: "",
- content: "",
- modalSizeClass: options.modalSizeClass,
- buttons: '',
- options: options.templateOptions,
- closeButtonLabel: _t("Close"),
- };
-
- // setup the Title
- if (options.title === null) {
- var $title = $(options.titleSelector, $raw);
- tplObject.title = $title.html();
- $(options.titleSelector, $raw).remove();
- } else {
- tplObject.title = options.title;
- }
-
- // Grab items to to insert into the prepend area
- if (options.prependContent) {
- tplObject.prepend = $("")
- .append($(options.prependContent, $raw).clone())
- .html();
- $(options.prependContent, $raw).remove();
- }
-
- // Filter out the content if there is a selector provided
- if (options.content) {
- tplObject.content = $(options.content, $raw).html();
- } else {
- tplObject.content = $raw.html();
- }
-
- // Render html
- self.$modal = $(
- _.template(self.options.templateOptions.template)(tplObject)
- );
- self.$modalDialog = $(
- "> ." + self.options.templateOptions.classDialog,
- self.$modal
- );
- self.$modalContent = $(
- "> ." + self.options.templateOptions.classModal,
- self.$modalDialog
- );
-
- // In most browsers, when you hit the enter key while a form element is focused
- // the browser will trigger the form 'submit' event. Google Chrome also does this,
- // but not when when the default submit button is hidden with 'display: none'.
- // The following code will work around this issue:
- $("form", self.$modal).on("keydown", function (event) {
- // ignore keys which are not enter, and ignore enter inside a textarea.
- if (event.key !== "Enter" || event.target.nodeName === "TEXTAREA") {
- return;
- }
- event.preventDefault();
- $("input[type=submit], button[type=submit], button:not(type)", this)
- .eq(0)
- .trigger("click");
- });
-
- // Setup buttons
- $(options.buttons, self.$modal).each(function () {
- var $button = $(this);
- $button
- .on("click", function (e) {
- e.stopPropagation();
- e.preventDefault();
- })
- .clone()
- .appendTo($(".pattern-modal-buttons", self.$modal))
- .off("click")
- .on("click", function (e) {
- e.stopPropagation();
- e.preventDefault();
- $button.trigger("click");
- });
- $button.hide();
- });
-
- self.emit("before-events-setup");
-
- // Wire up events
- self.$modal[0]
- .querySelectorAll(
- `.modal-header > .modal-close,
- .modal-footer > .pattern-modal-buttons > .modal-close,
- .modal-footer [name="form.buttons.Cancel" i]`
- )
- .forEach((el) => {
- $(el)
- .off("click")
- .on("click", (e) => {
- e.stopPropagation();
- e.preventDefault();
- $(e.target).trigger("destroy.plone-modal.patterns");
- });
- });
-
- // form
- if (options.form) {
- options.form.apply(self, [options.actions]);
- }
-
- self.$modal
- .addClass(self.options.templateOptions.className)
- .on("destroy.plone-modal.patterns", function (e) {
- e.stopPropagation();
- self.hide();
- })
- .on("resize.plone-modal.patterns", function (e) {
- e.stopPropagation();
- e.preventDefault();
- self.positionModal();
- })
- .appendTo(self.$wrapperInner);
-
- if (self.options.loadLinksWithinModal) {
- self.$modal.on("click", function (e) {
- e.stopPropagation();
- if ($.nodeName(e.target, "a")) {
- e.preventDefault();
- // TODO: open links inside modal
- // and slide modal body
- }
- self.$modal.trigger("modal-click");
- });
- }
- self.$modal.data("pattern-" + self.name, self);
- self.emit("after-render");
- if (options.onRender) {
- if (typeof options.onRender === "string") {
- window[options.onRender](self);
- } else {
- options.onRender(self);
- }
- }
- },
- },
- reloadWindow: function () {
- window.parent.location.reload();
- },
- init: function () {
- import("./modal.scss");
-
- var self = this;
- self.options.loadLinksWithinModal = $.parseJSON(
- self.options.loadLinksWithinModal
- );
-
- if (self.options.backdropOptions.closeOnEsc === true) {
- $(document).on("keydown", function (e) {
- if (self.$el.is("." + self.options.templateOptions.classActiveName)) {
- if (e.key === "Esc") {
- // ESC key pressed
- self.hide();
- }
- }
- });
- }
-
- $(window.parent).resize(function () {
- self.positionModal();
- });
-
- if (self.options.triggers) {
- $.each(self.options.triggers, function (i, item) {
- var e = item.substring(0, item.indexOf(" "));
- var selector = item.substring(item.indexOf(" "), item.length);
- $(selector || self.$el).on(e, function (e) {
- e.stopPropagation();
- e.preventDefault();
- self.show();
- });
- });
- }
-
- if (self.$el.is("a")) {
- if (self.$el.attr("href") && !self.options.image) {
- if (
- !self.options.target &&
- self.$el.attr("href").substr(0, 1) === "#" &&
- self.$el.attr("href").length > 1
- ) {
- self.options.target = self.$el.attr("href");
- self.options.content = "";
- }
- if (
- !self.options.ajaxUrl &&
- self.$el.attr("href").substr(0, 1) !== "#"
- ) {
- self.options.ajaxUrl = function () {
- // Resolve ``href`` attribute later, when modal is shown.
- return self.$el.attr("href");
- };
- }
- }
- self.$el.on("click", function (e) {
- e.stopPropagation();
- e.preventDefault();
- self.show();
- });
- }
- self.initModal();
- },
-
- createAjaxModal: function () {
- var self = this;
- self.emit("before-ajax");
- self.loading.show();
-
- var ajaxUrl = self.options.ajaxUrl;
- if (typeof ajaxUrl === "function") {
- ajaxUrl = ajaxUrl.apply(self, [self.options]);
- }
-
- self.ajaxXHR = $.ajax({
- url: ajaxUrl,
- type: self.options.ajaxType,
- })
- .done(function (response, textStatus, xhr) {
- self.ajaxXHR = undefined;
- self.$raw = $("").append($(utils.parseBodyTag(response)));
- self.emit("after-ajax", self, textStatus, xhr);
- self._show();
- })
- .fail(function (xhr, textStatus, errorStatus) {
- var options = self.options.actionOptions;
- if (textStatus === "timeout" && options.onTimeout) {
- options.onTimeout(self.$modal, xhr, errorStatus);
- } else if (options.onError) {
- options.onError(xhr, textStatus, errorStatus);
- } else {
- window.alert(_t("There was an error loading modal."));
- self.hide();
- }
- self.emit("linkActionError", [xhr, textStatus, errorStatus]);
- })
- .always(function () {
- self.loading.hide();
- });
- },
-
- createTargetModal: function () {
- var self = this;
- self.$raw = $(self.options.target).clone();
- self._show();
- },
-
- createBasicModal: function () {
- var self = this;
- self.$raw = $("").html(self.$el.clone());
- self._show();
- },
-
- createHtmlModal: function () {
- var self = this;
- var $el = $(self.options.html);
- self.$raw = $el;
- self._show();
- },
-
- createImageModal: function () {
- var self = this;
- self.$wrapper.addClass("image-modal");
- var src = self.$el.attr("href");
- var srcset = self.$el.attr("data-modal-srcset") || "";
- var title = $.trim(self.$el.text()) || "Image";
- // XXX aria?
- self.$raw = $(
- "'
- );
- self._show();
- },
-
- initModal: function () {
- var self = this;
- if (self.options.ajaxUrl) {
- self.createModal = self.createAjaxModal;
- } else if (self.options.target) {
- self.createModal = self.createTargetModal;
- } else if (self.options.html) {
- self.createModal = self.createHtmlModal;
- } else if (self.options.image) {
- self.createModal = self.createImageModal;
- } else {
- self.createModal = self.createBasicModal;
- }
- },
-
- findPosition: function (
- horpos,
- vertpos,
- margin,
- modalWidth,
- modalHeight,
- wrapperInnerWidth,
- wrapperInnerHeight
- ) {
- var returnpos = {};
- var absTop, absBottom, absLeft, absRight;
-
- // -- HORIZONTAL POSITION -----------------------------------------------
- if (horpos === "left") {
- absLeft = margin + "px";
- // if the width of the wrapper is smaller than the modal, and thus the
- // screen is smaller than the modal, force the left to simply be 0
- if (modalWidth > wrapperInnerWidth) {
- absLeft = "0px";
- }
- returnpos.left = absLeft;
- } else if (horpos === "right") {
- absRight = margin + "px";
- // if the width of the wrapper is smaller than the modal, and thus the
- // screen is smaller than the modal, force the right to simply be 0
- if (modalWidth > wrapperInnerWidth) {
- absRight = "0px";
- }
- returnpos.right = absRight;
- returnpos.left = "auto";
- }
- // default, no specified location, is to center
- else {
- absLeft = wrapperInnerWidth / 2 - modalWidth / 2 - margin + "px";
- // if the width of the wrapper is smaller than the modal, and thus the
- // screen is smaller than the modal, force the left to simply be 0
- if (modalWidth > wrapperInnerWidth) {
- absLeft = "0px";
- }
- returnpos.left = absLeft;
- }
-
- // -- VERTICAL POSITION -------------------------------------------------
- if (vertpos === "top") {
- absTop = margin + "px";
- // if the height of the wrapper is smaller than the modal, and thus the
- // screen is smaller than the modal, force the top to simply be 0
- if (modalHeight > wrapperInnerHeight) {
- absTop = "0px";
- }
- returnpos.top = absTop;
- } else if (vertpos === "bottom") {
- absBottom = margin + "px";
- // if the height of the wrapper is smaller than the modal, and thus the
- // screen is smaller than the modal, force the bottom to simply be 0
- if (modalHeight > wrapperInnerHeight) {
- absBottom = "0px";
- }
- returnpos.bottom = absBottom;
- returnpos.top = "auto";
- } else {
- // default case, no specified location, is to center
- absTop = wrapperInnerHeight / 2 - modalHeight / 2 - margin + "px";
- // if the height of the wrapper is smaller than the modal, and thus the
- // screen is smaller than the modal, force the top to simply be 0
- if (modalHeight > wrapperInnerHeight) {
- absTop = "0px";
- }
- returnpos.top = absTop;
- }
- return returnpos;
- },
-
- modalInitialized: function () {
- var self = this;
- return self.$modal !== null && self.$modal !== undefined;
- },
-
- activateFocusTrap: function () {
- var self = this;
- const modal_el = self.$modal[0];
- const focusable_selector = `select, input:not([type="hidden"]), textarea, button, a`;
-
- // Re-query visible focusable elements on each Tab press so that
- // dynamically loaded content (e.g. AJAX-loaded occurrence lists)
- // is always reachable via keyboard.
- function getVisibleInputs() {
- var bodyEl = modal_el.querySelector(
- `.${self.options.templateOptions.classBodyName}`
- );
- var footerEl = modal_el.querySelector(
- `.${self.options.templateOptions.classFooterName}`
- );
- var inputsBody = bodyEl
- ? bodyEl.querySelectorAll(focusable_selector)
- : [];
- var inputsFooter = footerEl
- ? footerEl.querySelectorAll(focusable_selector)
- : [];
- var inputs = [];
- for (const el of [...inputsBody, ...inputsFooter]) {
- if (dom.is_visible(el)) {
- inputs.push(el);
- }
- }
- if (inputs.length === 0) {
- inputs = [...modal_el.querySelectorAll(".modal-title")];
- }
- return inputs;
- }
-
- var closeInput = modal_el.querySelector(".modal-close");
-
- // Remove previous focus trap listener to prevent duplicates
- // when activateFocusTrap is called multiple times (e.g. redraw).
- if (self._focusTrapHandler) {
- modal_el.removeEventListener("keydown", self._focusTrapHandler);
- }
- self._focusTrapHandler = (e) => {
- if (e.key === "Tab") {
- e.preventDefault();
-
- var inputs = getVisibleInputs();
- var firstInput = inputs.length !== 0 ? inputs[0] : null;
- var lastInput = inputs.length !== 0 ? inputs[inputs.length - 1] : null;
- var target = e.target;
- var currentIndex = inputs.indexOf(target);
- if (currentIndex >= 0 && currentIndex < inputs.length) {
- var nextIndex = currentIndex + (e.shiftKey ? -1 : 1);
- if (nextIndex < 0 || nextIndex >= inputs.length) {
- closeInput.focus();
- } else {
- inputs[nextIndex].focus();
- }
- } else if (e.shiftKey && lastInput) {
- lastInput.focus();
- } else if (firstInput) {
- firstInput.focus();
- }
- }
- };
- modal_el.addEventListener("keydown", self._focusTrapHandler);
-
- if (self.options.backdropOptions.closeOnClick === true) {
- modal_el.addEventListener("click", (e) => {
- if (!e.target.closest(`.${self.options.templateOptions.classModal}`)) {
- self.hide();
- }
- });
- }
-
- var inputs = getVisibleInputs();
- var firstInput = inputs.length !== 0 ? inputs[0] : null;
- if (firstInput && ["INPUT", "SELECT", "TEXTAREA"].includes(firstInput.nodeName)) {
- // autofocus first element when opening a modal with a form
- firstInput.focus();
- }
- },
-
- positionModal: function () {
- /* re-position modal at any point.
- *
- * Uses:
- * options.margin
- * options.width
- * options.height
- * options.position
- */
- var self = this;
- // modal isn't initialized
- if (!self.modalInitialized()) {
- return;
- }
- // clear out any previously set styling
- self.$modal.removeAttr("style");
-
- // if backdrop wrapper is set on body, then wrapper should have height of
- // the window, so we can do scrolling of inner wrapper
- if (self.$wrapper.parent().is("body")) {
- self.$wrapper.height($(window.parent).height());
- }
-
- var margin =
- typeof self.options.margin === "function"
- ? self.options.margin()
- : self.options.margin;
- let modalCss = {
- position: "absolute",
- };
- if (margin !== "0") {
- modalCss["padding"] = margin;
- }
- self.$modal.css(modalCss);
- self.$modalDialog.css({
- // margin: "0",
- // padding: "0",
- width: self.options.width, // defaults to "", which doesn't override other css
- height: self.options.height, // defaults to "", which doesn't override other css
- });
- self.$modalContent.css({
- width: self.options.width, // defaults to "", which doesn't override other css
- });
-
- var posopt = self.options.position.split(" "),
- horpos = posopt[0],
- vertpos = posopt[1];
- var modalWidth = self.$modalDialog.outerWidth(true);
- var modalHeight = self.$modalDialog.outerHeight(true);
- var wrapperInnerWidth = self.$wrapperInner.width();
- var wrapperInnerHeight = self.$wrapperInner.height();
- var pos = self.findPosition(
- horpos,
- vertpos,
- margin,
- modalWidth,
- modalHeight,
- wrapperInnerWidth,
- wrapperInnerHeight
- );
- for (var key in pos) {
- self.$modalDialog.css(key, pos[key]);
- }
- },
-
- render: function (options) {
- var self = this;
- self.emit("render");
- self.options.render.apply(self, [options]);
- self.emit("rendered");
- },
-
- show: function () {
- var self = this;
- self.backdrop = self.createBackdrop();
- self.createModal();
- },
-
- createBackdrop: function () {
- var self = this,
- backdrop = new Backdrop(
- self.$el.parents(self.options.backdrop),
- self.options.backdropOptions
- ),
- zIndex = self.options.backdropOptions.zIndex || 1041;
-
- $(self.options.zIndexSelector).each(function () {
- zIndex = Math.max(zIndex, parseInt($(this).css("zIndex")) + 1 || 1041);
- });
-
- self.$wrapper = $("")
- .hide()
- .css({
- "z-index": zIndex,
- "overflow-y": "auto",
- "position": "fixed",
- "height": "100%",
- "width": "100%",
- "bottom": "0",
- "left": "0",
- "right": "0",
- "top": "0",
- })
- .addClass(self.options.templateOptions.classWrapperName)
- .insertBefore(backdrop.$backdrop)
- .on("click", function (e) {
- if (self.options.backdropOptions.closeOnClick) {
- e.stopPropagation();
- e.preventDefault();
- backdrop.hide();
- }
- });
- backdrop.on("hidden", function () {
- if (
- self.$modal !== undefined &&
- self.$modal.hasClass(self.options.templateOptions.classActiveName)
- ) {
- self.hide();
- }
- });
- self.loading = new utils.Loading({
- backdrop: backdrop,
- });
- self.$wrapperInner = $("")
- .addClass(self.options.classWrapperInnerName)
- .css({
- position: "absolute",
- bottom: "0",
- left: "0",
- right: "0",
- top: "0",
- })
- .appendTo(self.$wrapper);
- return backdrop;
- },
-
- _show: function () {
- var self = this;
- self.render.apply(self, [self.options]);
- self.emit("show");
- self.backdrop.show();
- self.$wrapper.show();
- self.loading.hide();
- self.$el.addClass(self.options.templateOptions.classActiveName);
- self.$modal.addClass(self.options.templateOptions.classActiveName);
- registry.scan(self.$modal);
- self.positionModal();
- $(window.parent).on("resize.plone-modal.patterns", function () {
- self.positionModal();
- });
- $("body").addClass("modal-open");
- self.emit("shown");
- self.activateFocusTrap();
- },
- hide: function () {
- var self = this;
- if (self.ajaxXHR) {
- self.ajaxXHR.abort();
- }
- self.emit("hide");
- if (self._suppressHide) {
- if (!window.confirm(self._suppressHide)) {
- return;
- }
- }
- self.loading.hide();
- self.$el.removeClass(self.options.templateOptions.classActiveName);
- if (self.$modal !== undefined) {
- self.$modal.remove();
- self.initModal();
- }
- self.$wrapper.remove();
- if ($(".modal", $("body")).length < 1) {
- self._suppressHide = undefined;
- self.backdrop.hide();
- $("body").removeClass("modal-open");
- $(window.parent).off("resize.plone-modal.patterns");
- }
- self.emit("hidden");
- self.$el.focus();
- },
- redraw: function (response, options) {
- var self = this;
- self.emit("beforeDraw");
- self.$modal.remove();
- self.$raw = $("").append($(utils.parseBodyTag(response)));
- self.render.apply(self, [options || self.options]);
- self.$modal.addClass(self.options.templateOptions.classActiveName);
- self.positionModal();
- registry.scan(self.$modal);
- self.emit("afterDraw");
- self.activateFocusTrap();
+ init: async function () {
+ // The implementation is a full Base.extend pattern (imperative callers
+ // do `new Modal(...)`), so its instance behaviour lives on the
+ // prototype — `Impl.init` would be the static registry initialiser.
+ const proto = (await import("./modal--implementation")).default.prototype;
+ // Defaults live with the implementation; merge them under the parsed
+ // options (which must win), reproducing the eager pattern's options.
+ this.options = $.extend(true, {}, proto.defaults, this.options);
+ // Graft onto this single instance so the pattern registers and behaves
+ // exactly as before, just with the heavy body loaded on demand.
+ $.extend(this, proto);
+ return proto.init.apply(this, arguments);
},
});
diff --git a/src/pat/querystring/querystring--implementation.js b/src/pat/querystring/querystring--implementation.js
new file mode 100644
index 0000000000..94279c4ff5
--- /dev/null
+++ b/src/pat/querystring/querystring--implementation.js
@@ -0,0 +1,930 @@
+import $ from "jquery";
+import _ from "underscore";
+import _t from "../../core/i18n-wrapper";
+import utils from "../../core/utils";
+import { Pattern as ContentbrowserPattern } from "../contentbrowser/contentbrowser";
+
+
+var Criteria = function () {
+ this.init.apply(this, arguments);
+};
+Criteria.prototype = {
+ defaults: {
+ indexWidth: "20em",
+ remove: "",
+ classBetweenDtName: "querystring-criteria-betweendt",
+ classWrapperName: "querystring-criteria-wrapper",
+ classIndexName: "querystring-criteria-index",
+ classOperatorName: "querystring-criteria-operator",
+ classValueName: "querystring-criteria-value",
+ classRemoveName: "querystring-criteria-remove",
+ classResultsName: "querystring-criteria-results",
+ classClearName: "querystring-criteria-clear",
+ classDepthName: "querystring-criteria-depth",
+ },
+ init: function (
+ $el,
+ app,
+ options,
+ indexes,
+ index,
+ operator,
+ value,
+ baseUrl,
+ patternDateOptions,
+ patternAjaxSelectOptions,
+ patternRelateditemsOptions
+ ) {
+ var self = this;
+ self.app = app;
+
+ self.options = {
+ ...self.defaults,
+ ...options,
+ };
+ self.indexes = indexes;
+ self.indexGroups = {};
+ self.baseUrl = baseUrl;
+ self.advanced = false;
+ self.initial = value;
+ // create wrapper criteria and append it to DOM
+ self.$wrapper = $("")
+ .addClass(self.options.classWrapperName)
+ .appendTo($el);
+
+ // Sub widgets options
+ self.patternDateOptions = patternDateOptions || {};
+ self.patternAjaxSelectOptions = patternAjaxSelectOptions || {};
+ self.patternRelateditemsOptions = patternRelateditemsOptions || {};
+ // Defaults
+ self.patternAjaxSelectOptions = {
+ width: "250px",
+ ...self.patternAjaxSelectOptions,
+ };
+ self.patternRelateditemsOptions = {
+ vocabularyUrl:
+ self.baseUrl +
+ "@@getVocabulary?name=plone.app.vocabularies.Catalog&field=relatedItems",
+ width: "20rem",
+ ...self.patternRelateditemsOptions,
+ };
+ // Force set
+ self.patternRelateditemsOptions["maximumSelectionSize"] = 1;
+
+ // Remove button
+ self.$remove = $("" + self.options.remove + "
")
+ .addClass(self.options.classRemoveName)
+ .appendTo(self.$wrapper)
+ .on("click", function (e) {
+ e.stopPropagation();
+ self.remove();
+ });
+
+ // Index selection
+ self.$index = $("").attr(
+ "placeholder",
+ _t("Select criteria")
+ );
+
+ // list of indexes
+ for (const value in self.indexes) {
+ let options = self.indexes[value];
+ if (options.enabled) {
+ if (!self.indexGroups[options.group]) {
+ self.indexGroups[options.group] = $("")
+ .attr("label", options.group)
+ .appendTo(self.$index);
+ }
+ self.indexGroups[options.group].append(
+ $("").attr("value", value).html(options.title)
+ );
+ }
+ }
+
+ // attach index select to DOM
+ self.$wrapper.append(
+ $("").addClass(self.options.classIndexName).append(self.$index)
+ );
+
+ // add blink (select2)
+ self.$index.patternSelect2({
+ width: self.options.indexWidth,
+ placeholder: _t("Select criteria"),
+ });
+ self.$index.on("change", function () {
+ // Read the value from the element rather than from the event's
+ // `val` property. Select2 v3 fires a jQuery `change` event carrying
+ // a `val` property, but pat-select2 also re-dispatches a native
+ // `change` event (for native listeners), which jQuery's `change`
+ // handler catches as well. On that second invocation `e.val` would
+ // be undefined. Reading `self.$index.val()` is correct in both cases.
+ self.removeValue();
+ self.createOperator(self.$index.val());
+ self.createClear();
+ self.trigger("index-changed");
+ });
+
+ if (typeof index !== "undefined") {
+ self.$index.val(index);
+ self.createOperator(index, operator, value);
+ self.createClear();
+ }
+
+ self.trigger("create-criteria");
+ },
+ appendOperators: function (index) {
+ var self = this;
+
+ self.$operator = $("");
+
+ if (self.indexes[index]) {
+ _.each(self.indexes[index].operations, function (value) {
+ var options = self.indexes[index].operators[value];
+ $("")
+ .attr("value", value)
+ .html(options.title)
+ .appendTo(self.$operator);
+ });
+ }
+
+ // attach operators select to DOM
+ self.$wrapper.append(
+ $("").addClass(self.options.classOperatorName).append(self.$operator)
+ );
+
+ // add blink (select2)
+ self.$operator.patternSelect2({ width: "10em" });
+ self.$operator.on("change", function () {
+ self.createValue(index);
+ self.createClear();
+ self.trigger("operator-changed");
+ });
+ },
+ convertPathOperators: function (oval) {
+ var self = this;
+
+ if (self.advanced) {
+ return oval;
+ }
+ //This allows us to use the same query operation for multiple dropdown options.
+ oval = oval.replace("advanced", "relativePath").replace("path", "relativePath");
+ return oval;
+ },
+ createPathOperators: function () {
+ var self = this;
+
+ if (self.advanced) {
+ self.resetPathOperators();
+ return;
+ }
+ var newOperator = "plone.app.querystring.operation.string.advanced";
+
+ if (typeof self.indexes.path.operators[newOperator] === "undefined") {
+ self.indexes.path.operations.push(newOperator);
+ self.indexes.path.operators[newOperator] = {
+ title: "Advanced",
+ widget: "AdvancedPathWidget",
+ description: "Enter a custom path string",
+ operation: "plone.app.querystring.queryparser._relativePath",
+ };
+ }
+
+ for (const key in self.indexes.path.operators) {
+ var options = self.indexes.path.operators[key];
+ if (key.indexOf("absolute") > 0) {
+ options.title = "Custom";
+ } else if (key.indexOf("relative") > 0) {
+ options.title = "Parent (../)";
+ } else if (key.indexOf("advanced") > 0) {
+ options.title = "Advanced Mode";
+ } else {
+ options.title = "Current (./)";
+ options.widget = "RelativePathWidget";
+ }
+ };
+ },
+ resetPathOperators: function () {
+ var self = this;
+ for (const key in self.indexes.path.operators) {
+ var options = self.indexes.path.operators[key];
+ if (key.indexOf("absolute") > 0) {
+ options.title = "Absolute Path";
+ } else if (key.indexOf("relative") > 0) {
+ options.title = "Relative Path";
+ } else if (key.indexOf("advanced") > 0) {
+ options.title = "Simple Mode";
+ } else {
+ options.title = "Navigation Path";
+ options.widget = "ReferenceWidget";
+ }
+ };
+
+ return;
+ },
+ createOperator: function (index, operator, value) {
+ var self = this;
+
+ self.removeOperator();
+ self.createPathOperators();
+
+ // We must test if we have a "simple" path or an "advanced" one and change the widgets accordingly
+ if (
+ index === "path" &&
+ value &&
+ value !== ".::1" &&
+ value !== "..::1" &&
+ !value.match(/^[0-9a-f\-]{32,36}::-?[0-9]+$/)
+ ) {
+ self.advanced = true;
+ self.resetPathOperators();
+ }
+
+ self.appendOperators(index);
+
+ if (typeof operator === "undefined") {
+ operator = self.$operator.val();
+ }
+
+ self.$operator.val(operator);
+ self.createValue(index, value);
+
+ self.trigger("create-operator");
+ },
+ createValue: function (index, value) {
+ var self = this,
+ widget = self.indexes[index].operators[self.$operator.val()].widget,
+ $wrapper = $("")
+ .addClass(self.options.classValueName)
+ .appendTo(self.$wrapper);
+
+ self.removeValue();
+
+ var createDepthSelect = function (selected) {
+ // remove previous depth-select-box items
+ $wrapper.remove(".depth-select-box");
+ var select =
+ "" +
+ "" +
+ "" + "
";
+
+ return $(select).on("change", function () {
+ self.trigger("depth-changed");
+ });
+ };
+
+ if (widget === "StringWidget") {
+ self.$value = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .val(value)
+ .appendTo($wrapper)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ } else if (widget === "DateWidget") {
+ self.$value = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .val(value)
+ .appendTo($wrapper)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ } else if (widget === "DateRangeWidget") {
+ var startwrap = $("").appendTo($wrapper);
+ var val1 = "";
+ var val2 = "";
+
+ if (value) {
+ val1 = value[0] ? value[0] : "";
+ val2 = value[1] ? value[1] : "";
+ }
+
+ var startdt = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .addClass(self.options.classValueName + "-" + widget + "-start")
+ .val(val1)
+ .appendTo(startwrap)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ $wrapper.append(
+ $("").html(_t("to")).addClass(self.options.classBetweenDtName)
+ );
+ var endwrap = $("").appendTo($wrapper);
+ var enddt = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .addClass(self.options.classValueName + "-" + widget + "-end")
+ .val(val2)
+ .appendTo(endwrap)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ self.$value = [startdt, enddt];
+ } else if (widget === "RelativeDateWidget") {
+ self.$value = $('')
+ .after($("").html(_t("days")))
+ .addClass(self.options.classValueName + "-" + widget)
+ .val(value)
+ .appendTo($wrapper)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ } else if (widget === "AdvancedPathWidget") {
+ if (self.advanced) {
+ self.advanced = false;
+ } else {
+ self.advanced = true;
+ }
+ self.createPathOperators();
+ self.removeOperator();
+ self.appendOperators(index);
+ self.createValue(index);
+ } else if (widget === "RelativePathWidget") {
+ if (self.advanced) {
+ self.$value = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .appendTo($wrapper)
+ .val(value)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ } else {
+ var pathAndDepth = [".", "1"];
+ if (typeof value !== "undefined" && value.indexOf("::") != -1) {
+ pathAndDepth = value.split("::");
+ if (pathAndDepth[0] === ".") {
+ self.$operator.val(
+ "plone.app.querystring.operation.string.path"
+ );
+ } else {
+ self.$operator.val(
+ "plone.app.querystring.operation.string.relativePath"
+ );
+ }
+ } else if (
+ self.$operator.val() ===
+ "plone.app.querystring.operation.string.relativePath"
+ ) {
+ pathAndDepth = ["..", "1"];
+ }
+
+ self.$value = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .appendTo($wrapper)
+ .val(pathAndDepth[0]);
+ self.$value.after(createDepthSelect(pathAndDepth[1]));
+ }
+ } else if (widget === "ReferenceWidget") {
+ if (self.advanced) {
+ self.$value = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .val(value)
+ .appendTo($wrapper)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ } else {
+ pathAndDepth = ["", "-1"];
+ if (typeof value !== "undefined") {
+ pathAndDepth = value.split("::");
+ }
+ self.$value = $('')
+ .addClass(self.options.classValueName + "-" + widget)
+ .appendTo($wrapper)
+ .val(pathAndDepth[0])
+ const pat = new ContentbrowserPattern(self.$value[0], self.patternRelateditemsOptions);
+ pat.el.addEventListener("change", () => {
+ self.trigger("value-changed");
+ })
+ self.$value.after(createDepthSelect(pathAndDepth[1]));
+ }
+ } else if (widget === "MultipleSelectionWidget") {
+ self.$value = $("")
+ .prop("multiple", true)
+ .addClass(self.options.classValueName + "-" + widget)
+ .appendTo($wrapper)
+ .on("change", function () {
+ self.trigger("value-changed");
+ });
+ if (self.indexes[index]) {
+ for (const value in self.indexes[index].values) {
+ const options = self.indexes[index].values[value];
+ $("")
+ .attr("value", value)
+ .html(options.title)
+ .appendTo(self.$value);
+ };
+ }
+ self.$value.patternSelect2(self.patternAjaxSelectOptions);
+ }
+
+ if (typeof value !== "undefined" && typeof self.$value !== "undefined") {
+ if (Array.isArray(self.$value)) {
+ $.each(value, function (i, v) {
+ self.$value[i].val(v);
+ });
+ } else {
+ var trimmedValue = value;
+ if (typeof value === "string" && widget !== "RelativePathWidget") {
+ trimmedValue = value.replace(/::-?[0-9]+/, "");
+ }
+ self.$value.val(trimmedValue);
+ }
+ }
+
+ self.trigger("create-value");
+ },
+ createClear: function () {
+ var self = this;
+ self.removeClear();
+ self.$clear = $("")
+ .addClass(self.options.classClearName)
+ .appendTo(self.$wrapper);
+ },
+ remove: function () {
+ var self = this;
+ self.trigger("remove");
+ self.$remove.remove();
+ self.$index.parent().remove();
+ self.removeOperator();
+ self.removeValue();
+ self.removeClear();
+ self.$wrapper.remove();
+ },
+ removeClear: function () {
+ var self = this;
+ self.trigger("remove-clear");
+ if (self.$clear) {
+ self.$clear.remove();
+ }
+ },
+ removeOperator: function () {
+ var self = this;
+ self.trigger("remove-operator");
+ if (self.$operator) {
+ self.$operator.parent().remove();
+ }
+ },
+ removeValue: function () {
+ var self = this;
+ self.trigger("remove-value");
+ if (self.$value) {
+ if (Array.isArray(self.$value)) {
+ // date ranges have 2 values
+ self.$value[0].parents(".querystring-criteria-value").remove();
+ } else {
+ self.$value.parents(".querystring-criteria-value").remove();
+ }
+ }
+ },
+ // builds the parameters to go into the http querystring for requesting
+ // results from the query builder
+ buildQueryPart: function () {
+ var self = this;
+
+ // index
+ var ival = self.$index.val();
+ if (ival === "") {
+ // no index selected, no query
+ return "";
+ }
+ var istr = "query.i:records=" + ival;
+
+ // operator
+ if (typeof self.$operator === "undefined") {
+ // no operator, no query
+ return "";
+ }
+ var oval = self.$operator.val();
+
+ if (ival === "path") {
+ if (oval.indexOf("advanced") > 0) {
+ return "";
+ }
+ oval = self.convertPathOperators(oval);
+ }
+
+ var ostr = "query.o:records=" + oval;
+
+ // value(s)
+ var vstrbase = "query.v:records=",
+ vstrlistbase = "query.v:records:list=",
+ vstr = [];
+ if (typeof self.$value === "undefined") {
+ vstr.push(vstrbase);
+ } else if (Array.isArray(self.$value)) {
+ // handles only datepickers from the 'between' operator right now
+ $.each(self.$value, function () {
+ vstr.push(vstrlistbase + $(this).val());
+ });
+ } else if (Array.isArray(self.$value.val())) {
+ // handles multible values
+ $.each(self.$value.val(), function (i, v) {
+ vstr.push(vstrlistbase + v);
+ });
+ } else {
+ var str = vstrbase + self.$value.val();
+ if (ival === "path" && self.$value.val() !== "") {
+ str += self.getDepthString();
+ } else if (typeof self.initial !== "undefined") {
+ str = vstrbase + self.initial;
+ //Sometimes the RelatedItemsWidget won't be loaded by this point.
+ //This only should happen on the initial page load.
+ delete self.initial;
+ }
+ vstr.push(str);
+ }
+
+ return istr + "&" + ostr + "&" + vstr.join("&");
+ },
+ getJSONListStr: function () {
+ var self = this;
+
+ // index
+ var ival = self.$index.val();
+ if (ival === "") {
+ // no index selected, no query
+ return "";
+ }
+
+ // operator
+ if (typeof self.$operator === "undefined") {
+ // no operator, no query
+ return "";
+ }
+ var oval = self.$operator.val();
+
+ if (ival === "path") {
+ if (oval.indexOf("advanced") > 0) {
+ //The advanced function is just a placeholder,
+ //We don't want to send an actual query
+ return "";
+ }
+ oval = self.convertPathOperators(oval);
+ }
+ // value(s)
+ var varr = [];
+ if (Array.isArray(self.$value)) {
+ // handles only datepickers from the 'between' operator right now
+ $.each(self.$value, function () {
+ varr.push($(this).val());
+ });
+ } else if (typeof self.$value !== "undefined") {
+ var value = self.$value.val();
+ if (ival === "path" && value) {
+ var depth = self.getDepthString();
+ if (depth) {
+ value += depth;
+ }
+ }
+ varr.push(value);
+ }
+ var vval;
+ if (varr.length > 1) {
+ vval = '["' + varr.join('","') + '"]';
+ } else if (varr.length === 1) {
+ vval = JSON.stringify(varr[0]);
+ } else {
+ vval = '""';
+ }
+
+ if (typeof self.indexes[ival].operators[oval] === "undefined") {
+ return;
+ }
+
+ return '{"i":"' + ival + '", "o":"' + oval + '", "v":' + vval + "}";
+ },
+ getDepthString: function () {
+ var self = this,
+ out = "",
+ depth = $("." + self.options.classDepthName).val();
+
+ if (depth !== "" && typeof depth !== "undefined") {
+ out += "::" + depth;
+ }
+ return out;
+ },
+ trigger: function (name) {
+ this.$wrapper.trigger(name + "-criteria.querystring.patterns", [this]);
+ },
+ on: function (name, callback) {
+ this.$wrapper.on(name + "-criteria.querystring.patterns", callback);
+ },
+};
+
+// Pattern config object — grafted onto the thin registered pattern in
+// querystring.js and run there. Kept out of the eager patterns chunk.
+export default {
+ defaults: {
+ indexes: [],
+ classWrapperName: "querystring-wrapper",
+ criteria: {},
+ indexOptionsUrl: null,
+ previewURL: "portal_factory/@@querybuilder_html_results", // base url to use to request preview information from
+ previewCountURL: "portal_factory/@@querybuildernumberofresults",
+ patternDateOptions: {},
+ patternAjaxSelectOptions: {},
+ patternRelateditemsOptions: {},
+ classSortLabelName: "querystring-sort-label",
+ classSortReverseName: "querystring-sortreverse",
+ classSortReverseLabelName: "querystring-sortreverse-label",
+ classPreviewCountWrapperName: "querystring-previewcount-wrapper",
+ classPreviewResultsWrapperName: "querystring-previewresults-wrapper",
+ classPreviewWrapperName: "querystring-preview-wrapper",
+ classPreviewName: "querystring-preview",
+ classPreviewTitleName: "querystring-preview-title",
+ classPreviewDescriptionName: "querystring-preview-description",
+ classSortWrapperName: "querystring-sort-wrapper",
+ showPreviews: true,
+ },
+ init: async function () {
+ await import("../select2/select2");
+
+ import("./querystring.scss");
+
+ var self = this;
+
+ // hide input element
+ self.$el.hide();
+
+ // create wrapper for out criteria
+ self.$wrapper = $("");
+ self.$el.after(self.$wrapper);
+
+ // initialization can be detailed if by ajax
+ self.initialized = false;
+
+ // get remove icon for criterias
+ self.options.criteria.remove = await utils.resolveIcon("x-circle");
+
+ if (self.options.indexOptionsUrl) {
+ try {
+ const response = await fetch(self.options.indexOptionsUrl);
+ const data = await response.json();
+ self.options.indexes = data.indexes;
+ self.options["sortable_indexes"] = data["sortable_indexes"];
+ self._init();
+ } catch {
+ // XXX handle this...
+ }
+ }
+ },
+ _init: function () {
+ var self = this;
+ self.$criteriaWrapper = $("")
+ .addClass(self.options.classWrapperName)
+ .appendTo(self.$wrapper);
+
+ self.$sortWrapper = $("")
+ .addClass(self.options.classSortWrapperName)
+ .appendTo(self.$wrapper);
+
+ if (self.options.showPreviews === "false") {
+ self.options.showPreviews = false;
+ }
+ if (self.options.showPreviews) {
+ self.$previewWrapper = $("")
+ .addClass(self.options.classPreviewWrapperName)
+ .appendTo(self.$wrapper);
+
+ // preview title and description
+ $("")
+ .addClass(self.options.classPreviewTitleName)
+ .html(_t("Preview"))
+ .appendTo(self.$previewWrapper);
+ $("")
+ .addClass(self.options.classPreviewDescriptionName)
+ .html(_t("Preview of at most 10 items"))
+ .appendTo(self.$previewWrapper);
+ }
+
+ self.criterias = [];
+
+ // create populated criterias
+ if (self.el.value) {
+ for (const item of JSON.parse(self.el.value)) {
+ self.createCriteria(item.i, item.o, item.v);
+ }
+ }
+
+ // add empty criteria which enables users to create new cr
+ self.createCriteria();
+
+ // add sort/order fields
+ self.createSort();
+
+ // add criteria preview pane to see results from criteria query
+ if (self.options.showPreviews) {
+ self.refreshPreviewEvent();
+ }
+ self.$el.trigger("initialized");
+ self.initialized = true;
+ },
+ createCriteria: function (index, operator, value) {
+ var self = this,
+ baseUrl = self.options.indexOptionsUrl.replace(/(@@.*)/g, ""),
+ criteria = new Criteria(
+ self.$criteriaWrapper,
+ self,
+ self.options.criteria,
+ self.options.indexes,
+ index,
+ operator,
+ value,
+ baseUrl,
+ self.options.patternDateOptions,
+ self.options.patternAjaxSelectOptions,
+ self.options.patternRelateditemsOptions
+ );
+
+ criteria.on("remove", function () {
+ if (self.criterias[self.criterias.length - 1] === criteria) {
+ self.createCriteria();
+ }
+ });
+
+ criteria.on("index-changed", function () {
+ if (self.criterias[self.criterias.length - 1] === criteria) {
+ self.createCriteria();
+ }
+ });
+
+ //This prevents multiple requests from going off after making a single change
+ var _doupdates = function () {
+ self.refreshPreviewEvent();
+ self.updateValue();
+ };
+ var _updateTimeout = -1;
+ var doupdates = function () {
+ clearTimeout(_updateTimeout);
+ _updateTimeout = setTimeout(_doupdates, 100);
+ };
+
+ criteria.on("remove", function (e, criteria) {
+ if (self.criterias.indexOf(criteria) !== -1) {
+ self.criterias.splice(self.criterias.indexOf(criteria), 1);
+ }
+ doupdates(e, criteria);
+ });
+ criteria.on("remove-clear", doupdates);
+ criteria.on("remove-operator", doupdates);
+ criteria.on("remove-value", doupdates);
+ criteria.on("index-changed", doupdates);
+ criteria.on("operator-changed", doupdates);
+ criteria.on("create-criteria", doupdates);
+ criteria.on("create-operator", doupdates);
+ criteria.on("create-value", doupdates);
+ criteria.on("value-changed", doupdates);
+ criteria.on("depth-changed", doupdates);
+
+ self.criterias.push(criteria);
+ },
+ createSort: function () {
+ var self = this;
+
+ // elements that may exist already on the page
+ // XXX do this in a way so it'll work with other forms will work
+ // as long as they provide sort_on and sort_reversed fields in z3c form
+ var existingSortOn = $('[id$="-sort_on"]').filter('[id^="formfield-"]');
+ var existingSortOrder = $('[id*="-sort_reversed"]').filter('[id^="formfield-"]');
+
+ $("")
+ .addClass(self.options.classSortLabelName)
+ .html(_t("Sort on"))
+ .appendTo(self.$sortWrapper);
+ self.$sortOn = $("")
+ .attr("name", "sort_on")
+ .appendTo(self.$sortWrapper)
+ .on("change", function () {
+ self.refreshPreviewEvent();
+ $('[id$="sort_on"]', existingSortOn).val($(this).val());
+ });
+
+ self.$sortOn.append($('")); // default no sorting
+ for (var key in self.options["sortable_indexes"]) {
+ self.$sortOn.append(
+ $("").attr("value", key).html(self.options.indexes[key].title)
+ );
+ }
+ self.$sortOn.patternSelect2({ width: "150px" });
+
+ self.$sortOrder = $('')
+ .attr("name", "sort_reversed:boolean")
+ .on("change", function () {
+ self.refreshPreviewEvent();
+ if ($(this).prop("checked")) {
+ $('input[type="checkbox"]', existingSortOrder).prop("checked", true);
+ } else {
+ $('input[type="checkbox"]', existingSortOrder).prop(
+ "checked",
+ false
+ );
+ }
+ });
+
+ $("")
+ .addClass(self.options.classSortReverseName)
+ .appendTo(self.$sortWrapper)
+ .append(self.$sortOrder)
+ .append(
+ $("")
+ .html(_t("Reversed Order"))
+ .addClass(self.options.classSortReverseLabelName)
+ );
+
+ // if the form already contains the sort fields, hide them! Their values
+ // will be synced back and forth between the querystring's form elements
+ if (existingSortOn.length >= 1 && existingSortOrder.length >= 1) {
+ var reversed = $('input[type="checkbox"]', existingSortOrder).prop(
+ "checked"
+ );
+ var sortOn = $('[id$="-sort_on"]', existingSortOn).val();
+ if (reversed) {
+ self.$sortOrder.prop("checked", true);
+ }
+ self.$sortOn.val(sortOn);
+ $(existingSortOn).hide();
+ $(existingSortOrder).hide();
+ }
+ },
+ refreshPreviewEvent: function () {
+ var self = this;
+
+ if (!self.options.showPreviews) {
+ return; // cut out of this if there are no previews available
+ }
+
+ if (typeof self._previewXhr !== "undefined") {
+ self._previewXhr.abort();
+ }
+
+ if (typeof self.$previewPane !== "undefined") {
+ self.$previewPane.remove();
+ }
+
+ var query = [];
+ for (const criteria of self.criterias) {
+ var querypart = criteria.buildQueryPart();
+ if (querypart !== "") {
+ query.push(querypart);
+ }
+ };
+
+ self.$previewPane = $("")
+ .addClass(self.options.classPreviewName)
+ .appendTo(self.$previewWrapper);
+
+ if (query.length <= 0) {
+ $("")
+ .addClass(self.options.classPreviewCountWrapperName)
+ .html("No results to preview")
+ .prependTo(self.$previewPane);
+ return; // no query means nothing to send out requests for
+ }
+
+ query.push("sort_on=" + self.$sortOn.val());
+ if (self.$sortOrder.prop("checked")) {
+ query.push("sort_order=reverse");
+ }
+
+ self._previewXhr = $.ajax({
+ url: self.options.previewURL + "?" + query.join("&"),
+ success: (data) => {
+ $("")
+ .addClass(self.options.classPreviewResultsWrapperName)
+ .html(utils.parseBodyTag(data))
+ .appendTo(self.$previewPane);
+ },
+ });
+ },
+ updateValue: function () {
+ // updating the original input with json data in the form:
+ // [
+ // {i:'index', o:'operator', v:'value'}
+ // ]
+
+ var self = this;
+
+ var criteriastrs = [];
+ for (const criteria of self.criterias) {
+ var jsonstr = criteria.getJSONListStr();
+ if (jsonstr !== "") {
+ criteriastrs.push(jsonstr);
+ }
+ }
+ var val = "[" + criteriastrs.join(",") + "]";
+ self.$el.val(val);
+ self.$el.trigger("change");
+ },
+};
diff --git a/src/pat/querystring/querystring.js b/src/pat/querystring/querystring.js
index ae19ca2042..bf161856c9 100644
--- a/src/pat/querystring/querystring.js
+++ b/src/pat/querystring/querystring.js
@@ -1,931 +1,23 @@
import $ from "jquery";
-import _ from "underscore";
-import _t from "../../core/i18n-wrapper";
-import utils from "../../core/utils";
import Base from "@patternslib/patternslib/src/core/base";
-import { Pattern as ContentbrowserPattern } from "../contentbrowser/contentbrowser";
-
-var Criteria = function () {
- this.init.apply(this, arguments);
-};
-Criteria.prototype = {
- defaults: {
- indexWidth: "20em",
- remove: "",
- classBetweenDtName: "querystring-criteria-betweendt",
- classWrapperName: "querystring-criteria-wrapper",
- classIndexName: "querystring-criteria-index",
- classOperatorName: "querystring-criteria-operator",
- classValueName: "querystring-criteria-value",
- classRemoveName: "querystring-criteria-remove",
- classResultsName: "querystring-criteria-results",
- classClearName: "querystring-criteria-clear",
- classDepthName: "querystring-criteria-depth",
- },
- init: function (
- $el,
- app,
- options,
- indexes,
- index,
- operator,
- value,
- baseUrl,
- patternDateOptions,
- patternAjaxSelectOptions,
- patternRelateditemsOptions
- ) {
- var self = this;
- self.app = app;
-
- self.options = {
- ...self.defaults,
- ...options,
- };
- self.indexes = indexes;
- self.indexGroups = {};
- self.baseUrl = baseUrl;
- self.advanced = false;
- self.initial = value;
- // create wrapper criteria and append it to DOM
- self.$wrapper = $("")
- .addClass(self.options.classWrapperName)
- .appendTo($el);
-
- // Sub widgets options
- self.patternDateOptions = patternDateOptions || {};
- self.patternAjaxSelectOptions = patternAjaxSelectOptions || {};
- self.patternRelateditemsOptions = patternRelateditemsOptions || {};
- // Defaults
- self.patternAjaxSelectOptions = {
- width: "250px",
- ...self.patternAjaxSelectOptions,
- };
- self.patternRelateditemsOptions = {
- vocabularyUrl:
- self.baseUrl +
- "@@getVocabulary?name=plone.app.vocabularies.Catalog&field=relatedItems",
- width: "20rem",
- ...self.patternRelateditemsOptions,
- };
- // Force set
- self.patternRelateditemsOptions["maximumSelectionSize"] = 1;
-
- // Remove button
- self.$remove = $("" + self.options.remove + "
")
- .addClass(self.options.classRemoveName)
- .appendTo(self.$wrapper)
- .on("click", function (e) {
- e.stopPropagation();
- self.remove();
- });
-
- // Index selection
- self.$index = $("").attr(
- "placeholder",
- _t("Select criteria")
- );
-
- // list of indexes
- for (const value in self.indexes) {
- let options = self.indexes[value];
- if (options.enabled) {
- if (!self.indexGroups[options.group]) {
- self.indexGroups[options.group] = $("")
- .attr("label", options.group)
- .appendTo(self.$index);
- }
- self.indexGroups[options.group].append(
- $("").attr("value", value).html(options.title)
- );
- }
- }
-
- // attach index select to DOM
- self.$wrapper.append(
- $("").addClass(self.options.classIndexName).append(self.$index)
- );
-
- // add blink (select2)
- self.$index.patternSelect2({
- width: self.options.indexWidth,
- placeholder: _t("Select criteria"),
- });
- self.$index.on("change", function () {
- // Read the value from the element rather than from the event's
- // `val` property. Select2 v3 fires a jQuery `change` event carrying
- // a `val` property, but pat-select2 also re-dispatches a native
- // `change` event (for native listeners), which jQuery's `change`
- // handler catches as well. On that second invocation `e.val` would
- // be undefined. Reading `self.$index.val()` is correct in both cases.
- self.removeValue();
- self.createOperator(self.$index.val());
- self.createClear();
- self.trigger("index-changed");
- });
-
- if (typeof index !== "undefined") {
- self.$index.val(index);
- self.createOperator(index, operator, value);
- self.createClear();
- }
-
- self.trigger("create-criteria");
- },
- appendOperators: function (index) {
- var self = this;
-
- self.$operator = $("");
-
- if (self.indexes[index]) {
- _.each(self.indexes[index].operations, function (value) {
- var options = self.indexes[index].operators[value];
- $("")
- .attr("value", value)
- .html(options.title)
- .appendTo(self.$operator);
- });
- }
-
- // attach operators select to DOM
- self.$wrapper.append(
- $("").addClass(self.options.classOperatorName).append(self.$operator)
- );
-
- // add blink (select2)
- self.$operator.patternSelect2({ width: "10em" });
- self.$operator.on("change", function () {
- self.createValue(index);
- self.createClear();
- self.trigger("operator-changed");
- });
- },
- convertPathOperators: function (oval) {
- var self = this;
-
- if (self.advanced) {
- return oval;
- }
- //This allows us to use the same query operation for multiple dropdown options.
- oval = oval.replace("advanced", "relativePath").replace("path", "relativePath");
- return oval;
- },
- createPathOperators: function () {
- var self = this;
-
- if (self.advanced) {
- self.resetPathOperators();
- return;
- }
- var newOperator = "plone.app.querystring.operation.string.advanced";
-
- if (typeof self.indexes.path.operators[newOperator] === "undefined") {
- self.indexes.path.operations.push(newOperator);
- self.indexes.path.operators[newOperator] = {
- title: "Advanced",
- widget: "AdvancedPathWidget",
- description: "Enter a custom path string",
- operation: "plone.app.querystring.queryparser._relativePath",
- };
- }
-
- for (const key in self.indexes.path.operators) {
- var options = self.indexes.path.operators[key];
- if (key.indexOf("absolute") > 0) {
- options.title = "Custom";
- } else if (key.indexOf("relative") > 0) {
- options.title = "Parent (../)";
- } else if (key.indexOf("advanced") > 0) {
- options.title = "Advanced Mode";
- } else {
- options.title = "Current (./)";
- options.widget = "RelativePathWidget";
- }
- };
- },
- resetPathOperators: function () {
- var self = this;
- for (const key in self.indexes.path.operators) {
- var options = self.indexes.path.operators[key];
- if (key.indexOf("absolute") > 0) {
- options.title = "Absolute Path";
- } else if (key.indexOf("relative") > 0) {
- options.title = "Relative Path";
- } else if (key.indexOf("advanced") > 0) {
- options.title = "Simple Mode";
- } else {
- options.title = "Navigation Path";
- options.widget = "ReferenceWidget";
- }
- };
-
- return;
- },
- createOperator: function (index, operator, value) {
- var self = this;
-
- self.removeOperator();
- self.createPathOperators();
-
- // We must test if we have a "simple" path or an "advanced" one and change the widgets accordingly
- if (
- index === "path" &&
- value &&
- value !== ".::1" &&
- value !== "..::1" &&
- !value.match(/^[0-9a-f\-]{32,36}::-?[0-9]+$/)
- ) {
- self.advanced = true;
- self.resetPathOperators();
- }
-
- self.appendOperators(index);
-
- if (typeof operator === "undefined") {
- operator = self.$operator.val();
- }
-
- self.$operator.val(operator);
- self.createValue(index, value);
-
- self.trigger("create-operator");
- },
- createValue: function (index, value) {
- var self = this,
- widget = self.indexes[index].operators[self.$operator.val()].widget,
- $wrapper = $("")
- .addClass(self.options.classValueName)
- .appendTo(self.$wrapper);
-
- self.removeValue();
-
- var createDepthSelect = function (selected) {
- // remove previous depth-select-box items
- $wrapper.remove(".depth-select-box");
- var select =
- "" +
- "" +
- "" + "
";
-
- return $(select).on("change", function () {
- self.trigger("depth-changed");
- });
- };
-
- if (widget === "StringWidget") {
- self.$value = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .val(value)
- .appendTo($wrapper)
- .on("change", function () {
- self.trigger("value-changed");
- });
- } else if (widget === "DateWidget") {
- self.$value = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .val(value)
- .appendTo($wrapper)
- .on("change", function () {
- self.trigger("value-changed");
- });
- } else if (widget === "DateRangeWidget") {
- var startwrap = $("").appendTo($wrapper);
- var val1 = "";
- var val2 = "";
-
- if (value) {
- val1 = value[0] ? value[0] : "";
- val2 = value[1] ? value[1] : "";
- }
-
- var startdt = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .addClass(self.options.classValueName + "-" + widget + "-start")
- .val(val1)
- .appendTo(startwrap)
- .on("change", function () {
- self.trigger("value-changed");
- });
- $wrapper.append(
- $("").html(_t("to")).addClass(self.options.classBetweenDtName)
- );
- var endwrap = $("").appendTo($wrapper);
- var enddt = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .addClass(self.options.classValueName + "-" + widget + "-end")
- .val(val2)
- .appendTo(endwrap)
- .on("change", function () {
- self.trigger("value-changed");
- });
- self.$value = [startdt, enddt];
- } else if (widget === "RelativeDateWidget") {
- self.$value = $('')
- .after($("").html(_t("days")))
- .addClass(self.options.classValueName + "-" + widget)
- .val(value)
- .appendTo($wrapper)
- .on("change", function () {
- self.trigger("value-changed");
- });
- } else if (widget === "AdvancedPathWidget") {
- if (self.advanced) {
- self.advanced = false;
- } else {
- self.advanced = true;
- }
- self.createPathOperators();
- self.removeOperator();
- self.appendOperators(index);
- self.createValue(index);
- } else if (widget === "RelativePathWidget") {
- if (self.advanced) {
- self.$value = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .appendTo($wrapper)
- .val(value)
- .on("change", function () {
- self.trigger("value-changed");
- });
- } else {
- var pathAndDepth = [".", "1"];
- if (typeof value !== "undefined" && value.indexOf("::") != -1) {
- pathAndDepth = value.split("::");
- if (pathAndDepth[0] === ".") {
- self.$operator.val(
- "plone.app.querystring.operation.string.path"
- );
- } else {
- self.$operator.val(
- "plone.app.querystring.operation.string.relativePath"
- );
- }
- } else if (
- self.$operator.val() ===
- "plone.app.querystring.operation.string.relativePath"
- ) {
- pathAndDepth = ["..", "1"];
- }
-
- self.$value = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .appendTo($wrapper)
- .val(pathAndDepth[0]);
- self.$value.after(createDepthSelect(pathAndDepth[1]));
- }
- } else if (widget === "ReferenceWidget") {
- if (self.advanced) {
- self.$value = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .val(value)
- .appendTo($wrapper)
- .on("change", function () {
- self.trigger("value-changed");
- });
- } else {
- pathAndDepth = ["", "-1"];
- if (typeof value !== "undefined") {
- pathAndDepth = value.split("::");
- }
- self.$value = $('')
- .addClass(self.options.classValueName + "-" + widget)
- .appendTo($wrapper)
- .val(pathAndDepth[0])
- const pat = new ContentbrowserPattern(self.$value[0], self.patternRelateditemsOptions);
- pat.el.addEventListener("change", () => {
- self.trigger("value-changed");
- })
- self.$value.after(createDepthSelect(pathAndDepth[1]));
- }
- } else if (widget === "MultipleSelectionWidget") {
- self.$value = $("")
- .prop("multiple", true)
- .addClass(self.options.classValueName + "-" + widget)
- .appendTo($wrapper)
- .on("change", function () {
- self.trigger("value-changed");
- });
- if (self.indexes[index]) {
- for (const value in self.indexes[index].values) {
- const options = self.indexes[index].values[value];
- $("")
- .attr("value", value)
- .html(options.title)
- .appendTo(self.$value);
- };
- }
- self.$value.patternSelect2(self.patternAjaxSelectOptions);
- }
-
- if (typeof value !== "undefined" && typeof self.$value !== "undefined") {
- if (Array.isArray(self.$value)) {
- $.each(value, function (i, v) {
- self.$value[i].val(v);
- });
- } else {
- var trimmedValue = value;
- if (typeof value === "string" && widget !== "RelativePathWidget") {
- trimmedValue = value.replace(/::-?[0-9]+/, "");
- }
- self.$value.val(trimmedValue);
- }
- }
-
- self.trigger("create-value");
- },
- createClear: function () {
- var self = this;
- self.removeClear();
- self.$clear = $("")
- .addClass(self.options.classClearName)
- .appendTo(self.$wrapper);
- },
- remove: function () {
- var self = this;
- self.trigger("remove");
- self.$remove.remove();
- self.$index.parent().remove();
- self.removeOperator();
- self.removeValue();
- self.removeClear();
- self.$wrapper.remove();
- },
- removeClear: function () {
- var self = this;
- self.trigger("remove-clear");
- if (self.$clear) {
- self.$clear.remove();
- }
- },
- removeOperator: function () {
- var self = this;
- self.trigger("remove-operator");
- if (self.$operator) {
- self.$operator.parent().remove();
- }
- },
- removeValue: function () {
- var self = this;
- self.trigger("remove-value");
- if (self.$value) {
- if (Array.isArray(self.$value)) {
- // date ranges have 2 values
- self.$value[0].parents(".querystring-criteria-value").remove();
- } else {
- self.$value.parents(".querystring-criteria-value").remove();
- }
- }
- },
- // builds the parameters to go into the http querystring for requesting
- // results from the query builder
- buildQueryPart: function () {
- var self = this;
-
- // index
- var ival = self.$index.val();
- if (ival === "") {
- // no index selected, no query
- return "";
- }
- var istr = "query.i:records=" + ival;
-
- // operator
- if (typeof self.$operator === "undefined") {
- // no operator, no query
- return "";
- }
- var oval = self.$operator.val();
-
- if (ival === "path") {
- if (oval.indexOf("advanced") > 0) {
- return "";
- }
- oval = self.convertPathOperators(oval);
- }
-
- var ostr = "query.o:records=" + oval;
-
- // value(s)
- var vstrbase = "query.v:records=",
- vstrlistbase = "query.v:records:list=",
- vstr = [];
- if (typeof self.$value === "undefined") {
- vstr.push(vstrbase);
- } else if (Array.isArray(self.$value)) {
- // handles only datepickers from the 'between' operator right now
- $.each(self.$value, function () {
- vstr.push(vstrlistbase + $(this).val());
- });
- } else if (Array.isArray(self.$value.val())) {
- // handles multible values
- $.each(self.$value.val(), function (i, v) {
- vstr.push(vstrlistbase + v);
- });
- } else {
- var str = vstrbase + self.$value.val();
- if (ival === "path" && self.$value.val() !== "") {
- str += self.getDepthString();
- } else if (typeof self.initial !== "undefined") {
- str = vstrbase + self.initial;
- //Sometimes the RelatedItemsWidget won't be loaded by this point.
- //This only should happen on the initial page load.
- delete self.initial;
- }
- vstr.push(str);
- }
-
- return istr + "&" + ostr + "&" + vstr.join("&");
- },
- getJSONListStr: function () {
- var self = this;
-
- // index
- var ival = self.$index.val();
- if (ival === "") {
- // no index selected, no query
- return "";
- }
-
- // operator
- if (typeof self.$operator === "undefined") {
- // no operator, no query
- return "";
- }
- var oval = self.$operator.val();
-
- if (ival === "path") {
- if (oval.indexOf("advanced") > 0) {
- //The advanced function is just a placeholder,
- //We don't want to send an actual query
- return "";
- }
- oval = self.convertPathOperators(oval);
- }
- // value(s)
- var varr = [];
- if (Array.isArray(self.$value)) {
- // handles only datepickers from the 'between' operator right now
- $.each(self.$value, function () {
- varr.push($(this).val());
- });
- } else if (typeof self.$value !== "undefined") {
- var value = self.$value.val();
- if (ival === "path" && value) {
- var depth = self.getDepthString();
- if (depth) {
- value += depth;
- }
- }
- varr.push(value);
- }
- var vval;
- if (varr.length > 1) {
- vval = '["' + varr.join('","') + '"]';
- } else if (varr.length === 1) {
- vval = JSON.stringify(varr[0]);
- } else {
- vval = '""';
- }
-
- if (typeof self.indexes[ival].operators[oval] === "undefined") {
- return;
- }
-
- return '{"i":"' + ival + '", "o":"' + oval + '", "v":' + vval + "}";
- },
- getDepthString: function () {
- var self = this,
- out = "",
- depth = $("." + self.options.classDepthName).val();
-
- if (depth !== "" && typeof depth !== "undefined") {
- out += "::" + depth;
- }
- return out;
- },
- trigger: function (name) {
- this.$wrapper.trigger(name + "-criteria.querystring.patterns", [this]);
- },
- on: function (name, callback) {
- this.$wrapper.on(name + "-criteria.querystring.patterns", callback);
- },
-};
+// Thin registration module: the widget implementation (incl. the eager edge
+// into pat-contentbrowser) is loaded lazily on first match, so it stays out of
+// the eager patterns chunk on pages without a querystring widget.
export default Base.extend({
name: "querystring",
trigger: ".pat-querystring",
parser: "mockup",
- defaults: {
- indexes: [],
- classWrapperName: "querystring-wrapper",
- criteria: {},
- indexOptionsUrl: null,
- previewURL: "portal_factory/@@querybuilder_html_results", // base url to use to request preview information from
- previewCountURL: "portal_factory/@@querybuildernumberofresults",
- patternDateOptions: {},
- patternAjaxSelectOptions: {},
- patternRelateditemsOptions: {},
- classSortLabelName: "querystring-sort-label",
- classSortReverseName: "querystring-sortreverse",
- classSortReverseLabelName: "querystring-sortreverse-label",
- classPreviewCountWrapperName: "querystring-previewcount-wrapper",
- classPreviewResultsWrapperName: "querystring-previewresults-wrapper",
- classPreviewWrapperName: "querystring-preview-wrapper",
- classPreviewName: "querystring-preview",
- classPreviewTitleName: "querystring-preview-title",
- classPreviewDescriptionName: "querystring-preview-description",
- classSortWrapperName: "querystring-sort-wrapper",
- showPreviews: true,
- },
- init: async function () {
- await import("../select2/select2");
-
- import("./querystring.scss");
-
- var self = this;
-
- // hide input element
- self.$el.hide();
-
- // create wrapper for out criteria
- self.$wrapper = $("");
- self.$el.after(self.$wrapper);
-
- // initialization can be detailed if by ajax
- self.initialized = false;
-
- // get remove icon for criterias
- self.options.criteria.remove = await utils.resolveIcon("x-circle");
-
- if (self.options.indexOptionsUrl) {
- try {
- const response = await fetch(self.options.indexOptionsUrl);
- const data = await response.json();
- self.options.indexes = data.indexes;
- self.options["sortable_indexes"] = data["sortable_indexes"];
- self._init();
- } catch {
- // XXX handle this...
- }
- }
- },
- _init: function () {
- var self = this;
- self.$criteriaWrapper = $("")
- .addClass(self.options.classWrapperName)
- .appendTo(self.$wrapper);
-
- self.$sortWrapper = $("")
- .addClass(self.options.classSortWrapperName)
- .appendTo(self.$wrapper);
-
- if (self.options.showPreviews === "false") {
- self.options.showPreviews = false;
- }
- if (self.options.showPreviews) {
- self.$previewWrapper = $("")
- .addClass(self.options.classPreviewWrapperName)
- .appendTo(self.$wrapper);
-
- // preview title and description
- $("")
- .addClass(self.options.classPreviewTitleName)
- .html(_t("Preview"))
- .appendTo(self.$previewWrapper);
- $("")
- .addClass(self.options.classPreviewDescriptionName)
- .html(_t("Preview of at most 10 items"))
- .appendTo(self.$previewWrapper);
- }
-
- self.criterias = [];
-
- // create populated criterias
- if (self.el.value) {
- for (const item of JSON.parse(self.el.value)) {
- self.createCriteria(item.i, item.o, item.v);
- }
- }
-
- // add empty criteria which enables users to create new cr
- self.createCriteria();
- // add sort/order fields
- self.createSort();
-
- // add criteria preview pane to see results from criteria query
- if (self.options.showPreviews) {
- self.refreshPreviewEvent();
- }
- self.$el.trigger("initialized");
- self.initialized = true;
- },
- createCriteria: function (index, operator, value) {
- var self = this,
- baseUrl = self.options.indexOptionsUrl.replace(/(@@.*)/g, ""),
- criteria = new Criteria(
- self.$criteriaWrapper,
- self,
- self.options.criteria,
- self.options.indexes,
- index,
- operator,
- value,
- baseUrl,
- self.options.patternDateOptions,
- self.options.patternAjaxSelectOptions,
- self.options.patternRelateditemsOptions
- );
-
- criteria.on("remove", function () {
- if (self.criterias[self.criterias.length - 1] === criteria) {
- self.createCriteria();
- }
- });
-
- criteria.on("index-changed", function () {
- if (self.criterias[self.criterias.length - 1] === criteria) {
- self.createCriteria();
- }
- });
-
- //This prevents multiple requests from going off after making a single change
- var _doupdates = function () {
- self.refreshPreviewEvent();
- self.updateValue();
- };
- var _updateTimeout = -1;
- var doupdates = function () {
- clearTimeout(_updateTimeout);
- _updateTimeout = setTimeout(_doupdates, 100);
- };
-
- criteria.on("remove", function (e, criteria) {
- if (self.criterias.indexOf(criteria) !== -1) {
- self.criterias.splice(self.criterias.indexOf(criteria), 1);
- }
- doupdates(e, criteria);
- });
- criteria.on("remove-clear", doupdates);
- criteria.on("remove-operator", doupdates);
- criteria.on("remove-value", doupdates);
- criteria.on("index-changed", doupdates);
- criteria.on("operator-changed", doupdates);
- criteria.on("create-criteria", doupdates);
- criteria.on("create-operator", doupdates);
- criteria.on("create-value", doupdates);
- criteria.on("value-changed", doupdates);
- criteria.on("depth-changed", doupdates);
-
- self.criterias.push(criteria);
- },
- createSort: function () {
- var self = this;
-
- // elements that may exist already on the page
- // XXX do this in a way so it'll work with other forms will work
- // as long as they provide sort_on and sort_reversed fields in z3c form
- var existingSortOn = $('[id$="-sort_on"]').filter('[id^="formfield-"]');
- var existingSortOrder = $('[id*="-sort_reversed"]').filter('[id^="formfield-"]');
-
- $("")
- .addClass(self.options.classSortLabelName)
- .html(_t("Sort on"))
- .appendTo(self.$sortWrapper);
- self.$sortOn = $("")
- .attr("name", "sort_on")
- .appendTo(self.$sortWrapper)
- .on("change", function () {
- self.refreshPreviewEvent();
- $('[id$="sort_on"]', existingSortOn).val($(this).val());
- });
-
- self.$sortOn.append($('")); // default no sorting
- for (var key in self.options["sortable_indexes"]) {
- self.$sortOn.append(
- $("").attr("value", key).html(self.options.indexes[key].title)
- );
- }
- self.$sortOn.patternSelect2({ width: "150px" });
-
- self.$sortOrder = $('')
- .attr("name", "sort_reversed:boolean")
- .on("change", function () {
- self.refreshPreviewEvent();
- if ($(this).prop("checked")) {
- $('input[type="checkbox"]', existingSortOrder).prop("checked", true);
- } else {
- $('input[type="checkbox"]', existingSortOrder).prop(
- "checked",
- false
- );
- }
- });
-
- $("")
- .addClass(self.options.classSortReverseName)
- .appendTo(self.$sortWrapper)
- .append(self.$sortOrder)
- .append(
- $("")
- .html(_t("Reversed Order"))
- .addClass(self.options.classSortReverseLabelName)
- );
-
- // if the form already contains the sort fields, hide them! Their values
- // will be synced back and forth between the querystring's form elements
- if (existingSortOn.length >= 1 && existingSortOrder.length >= 1) {
- var reversed = $('input[type="checkbox"]', existingSortOrder).prop(
- "checked"
- );
- var sortOn = $('[id$="-sort_on"]', existingSortOn).val();
- if (reversed) {
- self.$sortOrder.prop("checked", true);
- }
- self.$sortOn.val(sortOn);
- $(existingSortOn).hide();
- $(existingSortOrder).hide();
- }
- },
- refreshPreviewEvent: function () {
- var self = this;
-
- if (!self.options.showPreviews) {
- return; // cut out of this if there are no previews available
- }
-
- if (typeof self._previewXhr !== "undefined") {
- self._previewXhr.abort();
- }
-
- if (typeof self.$previewPane !== "undefined") {
- self.$previewPane.remove();
- }
-
- var query = [];
- for (const criteria of self.criterias) {
- var querypart = criteria.buildQueryPart();
- if (querypart !== "") {
- query.push(querypart);
- }
- };
-
- self.$previewPane = $("")
- .addClass(self.options.classPreviewName)
- .appendTo(self.$previewWrapper);
-
- if (query.length <= 0) {
- $("")
- .addClass(self.options.classPreviewCountWrapperName)
- .html("No results to preview")
- .prependTo(self.$previewPane);
- return; // no query means nothing to send out requests for
- }
-
- query.push("sort_on=" + self.$sortOn.val());
- if (self.$sortOrder.prop("checked")) {
- query.push("sort_order=reverse");
- }
-
- self._previewXhr = $.ajax({
- url: self.options.previewURL + "?" + query.join("&"),
- success: (data) => {
- $("")
- .addClass(self.options.classPreviewResultsWrapperName)
- .html(utils.parseBodyTag(data))
- .appendTo(self.$previewPane);
- },
- });
- },
- updateValue: function () {
- // updating the original input with json data in the form:
- // [
- // {i:'index', o:'operator', v:'value'}
- // ]
-
- var self = this;
-
- var criteriastrs = [];
- for (const criteria of self.criterias) {
- var jsonstr = criteria.getJSONListStr();
- if (jsonstr !== "") {
- criteriastrs.push(jsonstr);
- }
- }
- var val = "[" + criteriastrs.join(",") + "]";
- self.$el.val(val);
- self.$el.trigger("change");
+ init: async function () {
+ const impl = (await import("./querystring--implementation")).default;
+ // Defaults live with the implementation; merge them under the parsed
+ // options (which must win), reproducing the eager pattern's options.
+ this.options = $.extend(true, {}, impl.defaults, this.options);
+ // Graft the implementation's methods/state onto this single instance so
+ // external consumers (e.g. pat-structure's textfilter, which reads
+ // this.queryString.$sortOn) keep working against one object.
+ $.extend(this, impl);
+ return impl.init.apply(this, arguments);
},
});
diff --git a/src/pat/recurrence/recurrence--implementation.js b/src/pat/recurrence/recurrence--implementation.js
new file mode 100644
index 0000000000..b5cb185eba
--- /dev/null
+++ b/src/pat/recurrence/recurrence--implementation.js
@@ -0,0 +1,1463 @@
+import $ from "jquery";
+import _ from "underscore";
+import DisplayTemplate from "./templates/display.xml";
+import FormTemplate from "./templates/form.xml";
+import OccurrenceTemplate from "./templates/occurrence.xml";
+import Modal from "../modal/modal--implementation";
+import utils from "../../core/utils";
+
+// Formatting function (mostly) from jQueryTools dateinput
+var Re = /d{1,4}|m{1,4}|yy(?:yy)?|"[^"]*"|'[^']*'/g;
+
+function zeropad(val, len) {
+ val = val.toString();
+ len = len || 2;
+ while (val.length < len) {
+ val = "0" + val;
+ }
+ return val;
+}
+
+function format(date, fmt, conf) {
+ var d = date.getDate(),
+ D = date.getDay(),
+ m = date.getMonth(),
+ y = date.getFullYear(),
+ flags = {
+ d: d,
+ dd: zeropad(d),
+ ddd: conf.localization.shortWeekdays[D],
+ dddd: conf.localization.weekdays[D],
+ m: m + 1,
+ mm: zeropad(m + 1),
+ mmm: conf.localization.shortMonths[m],
+ mmmm: conf.localization.months[m],
+ yy: String(y).slice(2),
+ yyyy: y,
+ };
+
+ var result = fmt.replace(Re, function ($0) {
+ return Object.prototype.hasOwnProperty.call(flags, $0)
+ ? flags[$0]
+ : $0.slice(1, $0.length - 1);
+ });
+
+ return result;
+}
+
+function widgetSaveToRfc5545(form, RDATE, EXDATE, conf, tz) {
+ var value = form.find("select[name=rirtemplate]").val();
+ var rtemplate = conf.rtemplate[value];
+ var result = "RRULE:" + rtemplate.rrule;
+ var human = conf.localization.rtemplate[value];
+ var interval, month, year, occurrences, day;
+ var weekdays, i18nweekdays, input, monthlyType, index;
+ var yearlyType, rangeType;
+
+ for (const field_id of rtemplate.fields) {
+ const field = form.find(`#${field_id}`);
+
+ switch (field.attr("id")) {
+ case "ridailyinterval":
+ interval = field.find("input[name=ridailyinterval]").val();
+ if (interval !== "1") {
+ result += `;INTERVAL=${interval}`;
+ }
+ human = `${interval} ${conf.localization.dailyInterval2}`;
+ break;
+
+ case "riweeklyinterval":
+ interval = field.find("input[name=riweeklyinterval]").val();
+ if (interval !== "1") {
+ result += `;INTERVAL=${interval}`;
+ }
+ human = `${interval} ${conf.localization.weeklyInterval2}`;
+ break;
+
+ case "riweeklyweekdays":
+ weekdays = [];
+ i18nweekdays = [];
+ for (let j = 0; j < conf.weekdays.length; j++) {
+ input = field.find(`input[name=riweeklyweekdays${j}]`);
+ if (input.is(":checked")) {
+ weekdays.push(conf.weekdays[j]);
+ i18nweekdays.push(conf.localization.weekdays[j]);
+ }
+ }
+ if (weekdays.length > 0) {
+ result += `;BYDAY=${weekdays.join(",")}`;
+ human += ` ${
+ conf.localization.weeklyWeekdaysHuman
+ } ${i18nweekdays.join(",")}`;
+ }
+ break;
+
+ case "rimonthlyinterval":
+ interval = field.find("input[name=rimonthlyinterval]").val();
+ if (interval !== "1") {
+ result += `;INTERVAL=${interval}`;
+ }
+ human = `${interval} ${conf.localization.monthlyInterval2}`;
+ break;
+
+ case "rimonthlyoptions":
+ monthlyType = $("input[name=rimonthlytype]:checked", form).val();
+ if (monthlyType === "DAYOFMONTH") {
+ day = $("select[name=rimonthlydayofmonthday]", form).val();
+ result += `;BYMONTHDAY=${day}`;
+ human += `, ${conf.localization.monthlyDayOfMonth1Human} ${day} ${conf.localization.monthlyDayOfMonth2}`;
+ } else if (monthlyType === "WEEKDAYOFMONTH") {
+ index = $("select[name=rimonthlyweekdayofmonthindex]", form).val();
+ day =
+ conf.weekdays[
+ $("select[name=rimonthlyweekdayofmonth]", form).val()
+ ];
+ if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) {
+ result += `;BYDAY=${index}${day}`;
+ human += `, ${conf.localization.monthlyWeekdayOfMonth1Human} `;
+ human += `${
+ conf.localization.orderIndexes[
+ conf.orderIndexes.indexOf(index)
+ ]
+ } ${conf.localization.monthlyWeekdayOfMonth2} `;
+ human += `${
+ conf.localization.weekdays[conf.weekdays.indexOf(day)]
+ } ${conf.localization.monthlyDayOfMonth2}`;
+ }
+ }
+ break;
+
+ case "riyearlyinterval":
+ interval = field.find("input[name=riyearlyinterval]").val();
+ if (interval !== "1") {
+ result += `;INTERVAL=${interval}`;
+ }
+ human = `${interval} ${conf.localization.yearlyInterval2}`;
+ break;
+
+ case "riyearlyoptions":
+ yearlyType = $("input[name=riyearlyType]:checked", form).val();
+
+ if (yearlyType === "DAYOFMONTH") {
+ month = $("select[name=riyearlydayofmonthmonth]", form).val();
+ day = $("select[name=riyearlydayofmonthday]", form).val();
+ result += `;BYMONTH=${month};BYMONTHDAY=${day}`;
+ human += `, ${conf.localization.yearlyDayOfMonth1Human} ${
+ conf.localization.months[month - 1]
+ } ${day}`;
+ } else if (yearlyType === "WEEKDAYOFMONTH") {
+ index = $("select[name=riyearlyweekdayofmonthindex]", form).val();
+ day =
+ conf.weekdays[
+ $("select[name=riyearlyweekdayofmonthday]", form).val()
+ ];
+ month = $("select[name=riyearlyweekdayofmonthmonth]", form).val();
+ result += `;BYMONTH=${month}`;
+ if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) {
+ result += `;BYDAY=${index}${day}`;
+ human += ", " + conf.localization.yearlyWeekdayOfMonth1Human;
+ human +=
+ " " +
+ conf.localization.orderIndexes[
+ $.inArray(index, conf.orderIndexes)
+ ];
+ human += " " + conf.localization.yearlyWeekdayOfMonth2;
+ human +=
+ " " +
+ conf.localization.weekdays[$.inArray(day, conf.weekdays)];
+ human += " " + conf.localization.yearlyWeekdayOfMonth3;
+ human += " " + conf.localization.months[month - 1];
+ human += " " + conf.localization.yearlyWeekdayOfMonth4;
+ }
+ }
+ break;
+
+ case "rirangeoptions":
+ rangeType = form.find("input[name=rirangetype]:checked").val();
+ if (rangeType === "BYOCCURRENCES") {
+ occurrences = form
+ .find("input[name=rirangebyoccurrencesvalue]")
+ .val();
+ result += `;COUNT=${occurrences}`;
+ human += `, ${conf.localization.rangeByOccurrences1Human} ${occurrences} ${conf.localization.rangeByOccurrences2}`;
+ } else if (rangeType === "BYENDDATE") {
+ let date = form.find("input[name=rirangebyenddatecalendar]").val();
+ if (date === "") {
+ const today = new Date();
+ date = `${today.getFullYear()}-${(today.getMonth() + 1)
+ .toString()
+ .padStart(2, "0")}-${today.getDate()}`;
+ }
+ result += `;UNTIL=${date.replaceAll("-", "")}T000000${
+ tz === true ? "Z" : ""
+ }`;
+ human += `, ${conf.localization.rangeByEndDateHuman} `;
+ var date_parts = date.split("-");
+ human += format(
+ new Date(date_parts[0], date_parts[1] - 1, date_parts[2]),
+ conf.localization.longDateFormat,
+ conf,
+ );
+ }
+ break;
+ }
+ }
+
+ if (RDATE.length) {
+ RDATE.sort();
+ let tmp_dates = [];
+ let tmp_human = [];
+
+ // make sure our additional RDATE dates have the same start time
+ // XXX: not used, remove if superfluous
+ //const rdate_time = start_date
+ // ? `T${start_date.getHours()}:${start_date.getMinutes()}:00`
+ // : "T00:00:00";
+
+ for (let rdate of RDATE) {
+ if (rdate !== "") {
+ // The values from the input field are ISO8601 "YYYY-MM-DD"
+ // RFC5545 expects a date or date-time format of e.g.
+ // "YYYYMMDD". See:
+ // https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.2
+ rdate = rdate.replaceAll("-", "");
+ // by adding "T000000" the recurrence sequence generator of
+ // plone.event.recurrence adds the current start time correctly
+ rdate += rdate.length === 8 ? "T000000" : "";
+ rdate += tz ? "Z" : "";
+ tmp_dates.push(rdate);
+
+ // human readable RDATE
+ year = parseInt(rdate.substring(0, 4), 10);
+ month = parseInt(rdate.substring(4, 6), 10) - 1; // js month.
+ day = parseInt(rdate.substring(6, 8), 10);
+ tmp_human.push(
+ format(
+ new Date(year, month, day),
+ conf.localization.longDateFormat,
+ conf,
+ ),
+ );
+ }
+ }
+ if (tmp_dates.length) {
+ result += `\nRDATE:${tmp_dates.join(",")}`;
+ human += `${conf.localization.including} ${tmp_human.join("; ")}`;
+ }
+ }
+
+ if (EXDATE.length) {
+ EXDATE.sort();
+ let tmp_dates = [];
+ let tmp_human = [];
+ for (let exdate of EXDATE) {
+ if (exdate !== "") {
+ // EXDATE values are "YYYYMMDDTHHMMZ"
+ tmp_dates.push(exdate);
+
+ // human readable EXDATE
+ year = parseInt(exdate.substring(0, 4), 10);
+ month = parseInt(exdate.substring(4, 6), 10) - 1;
+ day = parseInt(exdate.substring(6, 8), 10);
+ tmp_human.push(
+ format(
+ new Date(year, month, day),
+ conf.localization.longDateFormat,
+ conf,
+ ),
+ );
+ }
+ }
+ if (tmp_dates.length) {
+ result += `\nEXDATE:${tmp_dates.join(",")}`;
+ human += `${conf.localization.except} ${tmp_human.join("; ")}`;
+ }
+ }
+
+ return { result: result, description: human };
+}
+
+function parseLine(icalline) {
+ var result = {};
+ var pos = icalline.indexOf(":");
+ var property = icalline.substring(0, pos);
+ result.value = icalline.substring(pos + 1);
+
+ if (property.indexOf(";") !== -1) {
+ pos = property.indexOf(";");
+ result.parameters = property.substring(pos + 1);
+ result.property = property.substring(0, pos);
+ } else {
+ result.parameters = null;
+ result.property = property;
+ }
+ return result;
+}
+
+function cleanDates(dates) {
+ // Get rid of timezones
+ // TODO: We could parse dates and range here, maybe?
+ var result = [];
+
+ for (const date of dates.split(",")) {
+ if (date.indexOf("Z") !== -1) {
+ result.push(date.substring(0, 15));
+ } else {
+ result.push(date);
+ }
+ }
+ return result;
+}
+
+function parseIcal(icaldata) {
+ var result = {
+ RRULE: "",
+ RDATE: [],
+ EXDATE: [],
+ };
+ var line = null;
+ var nextline;
+
+ var lines = icaldata.split("\n");
+ lines.reverse();
+ while (line !== "") {
+ if (lines.length > 0) {
+ nextline = lines.pop();
+ if (nextline.charAt(0) === " " || nextline.charAt(0) === "\t") {
+ // Line continuation:
+ line = line + nextline;
+ continue;
+ }
+ } else {
+ nextline = "";
+ }
+
+ // New line; the current one is finished, add it to the result.
+ if (line !== null) {
+ line = parseLine(line);
+ // We ignore properties for now
+ if (line.property === "RDATE" || line.property === "EXDATE") {
+ result[line.property] = cleanDates(line.value);
+ } else {
+ result[line.property] = line.value;
+ }
+ }
+
+ line = nextline;
+ }
+ return result;
+}
+
+function widgetLoadFromRfc5545(form, conf, icaldata, force) {
+ var unsupportedFeatures = [];
+ var matches, rtemplate, d, input, index;
+ var selectors, field, radiobutton;
+ var freq, interval, byday, bymonth, bymonthday, count, until;
+ var day, month, year, weekday;
+
+ if (icaldata.RRULE === undefined) {
+ unsupportedFeatures.push(conf.localization.noRule);
+ if (!force) {
+ return -1; // Fail!
+ }
+ } else {
+ matches = /FREQ=([^;]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ freq = matches[1];
+ } else {
+ freq = "DAILY";
+ }
+
+ matches = /INTERVAL=([0-9]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ interval = matches[1];
+ } else {
+ interval = "1";
+ }
+
+ matches = /BYDAY=([^;]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ byday = matches[1];
+ } else {
+ byday = "";
+ }
+
+ matches = /BYMONTHDAY=([^;]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ bymonthday = matches[1].split(",");
+ } else {
+ bymonthday = null;
+ }
+
+ matches = /BYMONTH=([^;]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ bymonth = matches[1].split(",");
+ } else {
+ bymonth = null;
+ }
+
+ matches = /COUNT=([0-9]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ count = matches[1];
+ } else {
+ count = null;
+ }
+
+ matches = /UNTIL=([0-9T]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ until = matches[1];
+ } else {
+ until = null;
+ }
+
+ matches = /BYSETPOS=([^;]+);?/.exec(icaldata.RRULE);
+ if (matches) {
+ unsupportedFeatures.push(conf.localization.bysetpos);
+ }
+
+ // Find the best rule:
+ if (Object.prototype.hasOwnProperty.call(conf.rtemplate, freq.toLowerCase())) {
+ rtemplate = conf.rtemplate[freq.toLowerCase()];
+ } else {
+ for (freq of conf.rtemplate) {
+ /* fallback to first available template if no match above*/
+ rtemplate = conf.rtemplate[freq];
+ break;
+ }
+ unsupportedFeatures.push(conf.localization.noTemplateMatch);
+ }
+
+ // set rirtemplate selector to computed value
+ form.find("select[name='rirtemplate']").val(freq.toLowerCase());
+
+ for (const field_id of rtemplate.fields) {
+ field = form.find(`#${field_id}`);
+ switch (field.attr("id")) {
+ case "ridailyinterval":
+ field.find("input[name=ridailyinterval]").val(interval);
+ break;
+
+ case "riweeklyinterval":
+ field.find("input[name=riweeklyinterval]").val(interval);
+ break;
+
+ case "riweeklyweekdays":
+ if (byday.length === 0) break;
+ byday = byday.split(",");
+ for (d = 0; d < conf.weekdays.length; d++) {
+ input = field.find(`input[name="riweeklyweekdays${d}"]`);
+ if (input.length === 0) continue;
+ day = conf.weekdays[d];
+ input.attr("checked", byday.includes(day));
+ }
+ break;
+
+ case "rimonthlyinterval":
+ field.find("input[name=rimonthlyinterval]").val(interval);
+ break;
+
+ case "rimonthlyoptions":
+ var monthlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY
+
+ if (bymonthday) {
+ monthlyType = "DAYOFMONTH";
+ if (bymonthday.length > 1) {
+ // No support for multiple days in one month
+ unsupportedFeatures.push(
+ conf.localization.multipleDayOfMonth,
+ );
+ // Just keep the first
+ bymonthday = bymonthday[0];
+ }
+ field
+ .find("select[name=rimonthlydayofmonthday]")
+ .val(bymonthday);
+ }
+
+ if (byday) {
+ monthlyType = "WEEKDAYOFMONTH";
+
+ if (byday.indexOf(",") !== -1) {
+ // No support for multiple days in one month
+ unsupportedFeatures.push(
+ conf.localization.multipleDayOfMonth,
+ );
+ byday = byday.split(",")[0];
+ }
+ index = byday.slice(0, -2);
+ if (index.charAt(0) !== "+" && index.charAt(0) !== "-") {
+ index = "+" + index;
+ }
+ weekday = byday.slice(-2);
+ field
+ .find("select[name=rimonthlyweekdayofmonthindex]")
+ .val(index);
+ const day_index = conf.weekdays.indexOf(weekday);
+ field
+ .find("select[name=rimonthlyweekdayofmonth]")
+ .val(day_index);
+ }
+
+ selectors = field.find("input[name=rimonthlytype]");
+ for (index = 0; index < selectors.length; index++) {
+ radiobutton = selectors[index];
+ $(radiobutton).attr(
+ "checked",
+ radiobutton.value === monthlyType,
+ );
+ }
+ break;
+
+ case "riyearlyinterval":
+ field.find("input[name=riyearlyinterval]").val(interval);
+ break;
+
+ case "riyearlyoptions":
+ var yearlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY
+
+ if (bymonthday) {
+ yearlyType = "DAYOFMONTH";
+ if (bymonthday.length > 1) {
+ // No support for multiple days in one month
+ unsupportedFeatures.push(
+ conf.localization.multipleDayOfMonth,
+ );
+ bymonthday = bymonthday[0];
+ }
+ field.find("select[name=riyearlydayofmonthmonth]").val(bymonth);
+ field.find("select[name=riyearlydayofmonthday]").val(bymonthday);
+ }
+
+ if (byday) {
+ yearlyType = "WEEKDAYOFMONTH";
+
+ if (byday.indexOf(",") !== -1) {
+ // No support for multiple days in one month
+ unsupportedFeatures.push(
+ conf.localization.multipleDayOfMonth,
+ );
+ byday = byday.split(",")[0];
+ }
+ index = byday.slice(0, -2);
+ if (index.charAt(0) !== "+" && index.charAt(0) !== "-") {
+ index = "+" + index;
+ }
+ weekday = byday.slice(-2);
+ field
+ .find("select[name=riyearlyweekdayofmonthindex]")
+ .val(index);
+
+ const weekday_index = conf.weekdays.indexOf(weekday);
+ field
+ .find("select[name=riyearlyweekdayofmonthday]")
+ .val(weekday_index);
+ field
+ .find("select[name=riyearlyweekdayofmonthmonth]")
+ .val(bymonth);
+ }
+
+ selectors = field.find("input[name=riyearlyType]");
+ for (index = 0; index < selectors.length; index++) {
+ radiobutton = selectors[index];
+ $(radiobutton).attr("checked", radiobutton.value === yearlyType);
+ }
+ break;
+
+ case "rirangeoptions":
+ // default value per configuration
+ var rangeType = conf.hasRepeatForeverButton
+ ? "NOENDDATE"
+ : "BYOCCURRENCES";
+
+ if (count) {
+ rangeType = "BYOCCURRENCES";
+ field.find("input[name=rirangebyoccurrencesvalue]").val(count);
+ }
+
+ if (until) {
+ rangeType = "BYENDDATE";
+ input = field.find("input[name=rirangebyenddatecalendar]");
+ year = until.slice(0, 4);
+ month = until.slice(4, 6);
+ day = until.slice(6, 8);
+ input.val(`${year}-${month}-${day}`);
+ }
+
+ selectors = field.find("input[name=rirangetype]");
+ for (index = 0; index < selectors.length; index++) {
+ radiobutton = selectors[index];
+ $(radiobutton).attr("checked", radiobutton.value === rangeType);
+ }
+ break;
+ }
+ }
+ }
+
+ var messagearea = form.find("#messagearea");
+ if (unsupportedFeatures.length !== 0) {
+ messagearea.text(
+ conf.localization.unsupportedFeatures + " " + unsupportedFeatures.join("; "),
+ );
+ messagearea.show();
+ return 1;
+ } else {
+ messagearea.text("");
+ messagearea.hide();
+ return 0;
+ }
+}
+
+const RecurrenceInput = function (conf, textarea) {
+ var self = this;
+ var $textarea = $(textarea);
+
+ // initalize parsed icaldata
+ if (textarea.innerHTML) {
+ textarea["ical"] = parseIcal(textarea.innerHTML);
+ } else {
+ textarea["ical"] = {
+ RRULE: "",
+ RDATE: [],
+ EXDATE: [],
+ };
+ }
+
+ // Extend conf with non-configurable data used by templates.
+ var orderedWeekdays = [];
+ var now_date = new Date().toISOString().substring(0, 10);
+ var index;
+
+ for (let i = 0; i < 7; i++) {
+ index = i + conf.firstDay;
+ if (index > 6) {
+ index = index - 7;
+ }
+ orderedWeekdays.push(index);
+ }
+
+ conf = {
+ ...conf,
+ orderIndexes: ["+1", "+2", "+3", "+4", "-1"],
+ weekdays: ["SU", "MO", "TU", "WE", "TH", "FR", "SA"],
+ orderedWeekdays: orderedWeekdays,
+ };
+
+ // The recurrence type dropdown should show certain fields depending
+ // on selection:
+ function displayFields(selector) {
+ // First hide all the fields
+ self.$modalForm.find(".rifield").hide();
+ // Then show the ones that should be shown.
+ var value = selector.val();
+ if (value) {
+ for (const rtField of conf.rtemplate[value].fields) {
+ self.$modalForm.find(`#${rtField}`).show();
+ }
+ }
+ }
+
+ function occurrenceExclude(event) {
+ event.preventDefault();
+ textarea["ical"].EXDATE.push(this.attributes.date.value);
+ var $this = $(this);
+ $this.addClass("exdate");
+ $this.parent().parent().addClass("exdate");
+ $this.off("click").on("click", occurrenceInclude);
+ }
+
+ function occurrenceInclude(event) {
+ event.preventDefault();
+ textarea["ical"].EXDATE.splice(
+ $.inArray(this.attributes.date.value, textarea["ical"].EXDATE),
+ 1,
+ );
+ var $this = $(this);
+ $this.removeClass("exdate");
+ $this.parent().parent().removeClass("exdate");
+ $this.off("click").on("click", occurrenceExclude);
+ }
+
+ function occurrenceDelete(event) {
+ event.preventDefault();
+ textarea["ical"].RDATE.splice(
+ $.inArray(this.attributes.date.value, textarea["ical"].RDATE),
+ 1,
+ );
+ $(this)
+ .parent()
+ .parent()
+ .hide("slow", function () {
+ $(this).remove();
+ });
+ }
+
+ function occurrenceAdd(event) {
+ event.preventDefault();
+ var datevalue = self.$modalForm.find(".riaddoccurrence input#adddate").val();
+ var date_parts = datevalue.split("-");
+ datevalue += datevalue.length === 10 ? "T000000" : "";
+ var errorarea = self.$modalForm.find(".riaddoccurrence div.alert");
+ errorarea.text("");
+ errorarea.hide();
+
+ // Add date only if it is not already in RDATE
+ if (!textarea["ical"].RDATE.includes(datevalue)) {
+ textarea["ical"].RDATE.push(datevalue);
+ var $newdate =
+ $(`
+
+ ${format(
+ new Date(date_parts[0], date_parts[1] - 1, date_parts[2]),
+ conf.localization.longDateFormat,
+ conf,
+ )},
+ ${conf.localization.additionalDate}
+
+
+
+
+
`);
+ $newdate.hide();
+ self.$modalForm.find("div.rioccurrences").prepend($newdate);
+ $newdate.slideDown();
+ $newdate.find("button.rdate").on("click", occurrenceDelete);
+ } else {
+ errorarea.text(conf.localization.alreadyAdded).show();
+ }
+ }
+
+ // element is where to find the tag in question. Can be the form
+ // or the display widget. Defaults to the self.$modalForm.
+ function loadOccurrences(startdate, rfc5545, start, readonly) {
+ var element, occurrenceDiv;
+
+ if (!readonly) {
+ element = self.$modalForm;
+ } else {
+ element = self.display;
+ }
+
+ occurrenceDiv = element.find(".rioccurrences");
+
+ const year = startdate.getFullYear();
+ const month = startdate.getMonth() + 1;
+ const day = startdate.getDate();
+
+ var data = {
+ year: year,
+ month: month, // Sending January as 0? I think not.
+ day: day,
+ rrule: rfc5545,
+ format: conf.localization.longDateFormat,
+ start: start,
+ };
+
+ $.ajax({
+ url: conf.ajaxURL,
+ async: false, // Can't be tested if it's asynchronous, annoyingly.
+ type: "post",
+ dataType: "json",
+ contentType: conf.ajaxContentType,
+ cache: false,
+ data: data,
+ success: function (resp) {
+ var result;
+
+ resp.readOnly = readonly;
+ resp.localization = conf.localization;
+ resp.icons = conf.icons;
+
+ // Format dates:
+ var date, y, m, d;
+ for (let occurrence of resp.occurrences) {
+ date = occurrence.date;
+ y = parseInt(date.substring(0, 4), 10);
+ m = parseInt(date.substring(4, 6), 10) - 1; // jan=0
+ d = parseInt(date.substring(6, 8), 10);
+ occurrence.date = `${y}-${zeropad(m + 1)}-${zeropad(d)}T000000`;
+ occurrence.formattedDate = format(
+ new Date(y, m, d),
+ conf.localization.longDateFormat,
+ conf,
+ );
+ }
+
+ result = _.template(OccurrenceTemplate)(resp);
+ occurrenceDiv.replaceWith(result);
+
+ // Add the batch actions:
+ element.find(".rioccurrences .batching a").on("click", function (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ loadOccurrences(
+ startdate,
+ rfc5545,
+ this.attributes.start.value,
+ readonly,
+ );
+ });
+
+ // Add the delete/undelete actions:
+ if (!readonly) {
+ element
+ .find(".rioccurrences .action button.rrule")
+ .on("click", occurrenceExclude);
+ element
+ .find(".rioccurrences .action button.exdate")
+ .on("click", occurrenceInclude);
+ element
+ .find(".rioccurrences .action button.rdate")
+ .on("click", occurrenceDelete);
+ }
+ },
+ error: function () {
+ occurrenceDiv[0].innerHTML = `
+ ${conf.localization.error_load_occurrences}
+ `;
+ },
+ });
+ }
+
+ function getField(field) {
+ // See if it is a field already
+ var realField = $(field);
+ if (!realField.length) {
+ // Otherwise, we assume it's an id:
+ realField = $("#" + field);
+ }
+ if (!realField.length) {
+ // Still not? Then it's a name.
+ realField = $("input[name='" + field + "']");
+ }
+ return realField;
+ }
+ function findStartDate() {
+ var startdate = null;
+ var startField, startFieldYear, startFieldMonth, startFieldDay;
+
+ // Find the default byday and bymonthday from the start date, if any:
+ if (conf.startField) {
+ startField = getField(conf.startField);
+ if (!startField.length) {
+ // Field not found
+ return null;
+ }
+ startdate = startField.val();
+ if (startdate === "") {
+ // Probably not an input at all. Try to see if it contains a date
+ startdate = startField.text();
+ }
+
+ if (typeof startdate === "string") {
+ // convert human readable, non ISO8601 dates, like
+ // '2014-04-24 19:00', where the 'T' separator is missing.
+ startdate = startdate.replace(" ", "T");
+ }
+
+ startdate = new Date(startdate);
+ } else if (conf.startFieldYear && conf.startFieldMonth && conf.startFieldDay) {
+ startFieldYear = getField(conf.startFieldYear);
+ startFieldMonth = getField(conf.startFieldMonth);
+ startFieldDay = getField(conf.startFieldDay);
+ if (
+ !startFieldYear.length &&
+ !startFieldMonth.length &&
+ !startFieldDay.length
+ ) {
+ // Field not found
+ return null;
+ }
+ startdate = new Date(
+ startFieldYear.val(),
+ startFieldMonth.val() - 1,
+ startFieldDay.val(),
+ );
+ }
+ if (startdate === null) {
+ return null;
+ }
+ // We have some sort of startdate:
+ if (isNaN(startdate)) {
+ return null;
+ }
+ return startdate;
+ }
+ function findEndDate() {
+ var endField, enddate;
+
+ endField = self.$modalForm.find("input[name=rirangebyenddatecalendar]");
+ enddate = endField.val();
+ enddate = new Date(enddate);
+
+ // if the end date is incorrect or the field is left empty
+ if (isNaN(enddate) || endField.val() === "") {
+ return null;
+ }
+ return enddate;
+ }
+ function findIntField(fieldName) {
+ var field, num;
+
+ field = self.$modalForm.find("input[name=" + fieldName + "]");
+
+ num = field.val();
+
+ // if it's not a number or the field is left empty
+ if (isNaN(num) || num.toString().indexOf(".") !== -1 || field.val() === "") {
+ return null;
+ }
+ return num;
+ }
+
+ // Loading (populating) display and form widget with
+ // passed RFC5545 string (data)
+ function loadData(form) {
+ widgetLoadFromRfc5545(form, conf, textarea["ical"], true);
+
+ const startdate = findStartDate();
+
+ if (startdate !== null) {
+ // If the date is a real date, set the defaults in the form
+ document.querySelector("select[name=rimonthlydayofmonthday]").value =
+ startdate.getDate();
+ const dayindex =
+ conf.orderIndexes[Math.floor((startdate.getDate() - 1) / 7)];
+ const day = conf.weekdays[startdate.getDay()];
+ document.querySelector("select[name=rimonthlyweekdayofmonthindex]").value =
+ dayindex;
+ document.querySelector("select[name=rimonthlyweekdayofmonth]").value = day;
+
+ document.querySelector("select[name=riyearlydayofmonthmonth]").value =
+ startdate.getMonth() + 1;
+ document.querySelector("select[name=riyearlydayofmonthday]").value =
+ startdate.getDate();
+ document.querySelector("select[name=riyearlyweekdayofmonthindex]").value =
+ dayindex;
+ document.querySelector("select[name=riyearlyweekdayofmonthday]").value = day;
+ document.querySelector("select[name=riyearlyweekdayofmonthmonth]").value =
+ startdate.getMonth() + 1;
+
+ // Now when we have a start date, we can also do an ajax call to calculate occurrences:
+ var rfc5545 =
+ textarea.innerHTML ||
+ widgetSaveToRfc5545(
+ form,
+ textarea["ical"].RDATE,
+ textarea["ical"].EXDATE,
+ conf,
+ false,
+ ).result;
+
+ loadOccurrences(startdate, rfc5545, 0, false);
+
+ // Show the add and refresh buttons:
+ document.querySelector("div.rioccurrencesactions").style.display = "block";
+ } else {
+ // No EXDATE/RDATE support
+ document.querySelector("div.rioccurrencesactions").style.display = "none";
+ }
+
+ displayFields(form.find("select[name=rirtemplate]"));
+ }
+
+ function recurrenceOn(form) {
+ var RFC5545 = widgetSaveToRfc5545(
+ form,
+ textarea["ical"].RDATE,
+ textarea["ical"].EXDATE,
+ conf,
+ false,
+ );
+ var label = self.display.find("label[class=ridisplay-label]");
+ label.text(conf.localization.displayActivate + " " + RFC5545.description);
+ textarea["ical"] = parseIcal(RFC5545.result);
+ textarea.innerHTML = RFC5545.result;
+ $textarea.trigger("change");
+ var startdate = findStartDate();
+ if (startdate !== null) {
+ loadOccurrences(startdate, RFC5545.result, 0, true);
+ }
+ self.display.find('button[name="riedit"]').text(conf.localization.edit_rules);
+ self.display.find('button[name="ridelete"]').show();
+ }
+
+ function recurrenceOff() {
+ var label = self.display.find("label[class=ridisplay-label]");
+ label.text(conf.localization.displayUnactivate);
+ // reset ical object
+ textarea["ical"] = {
+ RRULE: "",
+ RDATE: [],
+ EXDATE: [],
+ };
+ textarea.innerHTML = "";
+ $textarea.trigger("change"); // Clear the textarea.
+ self.display.find(".rioccurrences").hide();
+ self.display.find('button[name="riedit"]').text(conf.localization.add_rules);
+ self.display.find('button[name="ridelete"]').hide();
+ }
+
+ function checkFields(form) {
+ var startDate, endDate, num, messagearea;
+ startDate = findStartDate();
+
+ // Hide any error message from before
+ messagearea = self.$modalForm.find("#messagearea");
+ messagearea.text("");
+ messagearea.hide();
+
+ // Hide add field errors
+ self.$modalForm.find(".riaddoccurrence div.alert").text("").hide();
+
+ // Repeats Daily
+ if (self.$modalForm.find("#ridailyinterval").css("display") === "block") {
+ // Check repeat every field
+ num = findIntField("ridailyinterval", form);
+ if (!num || num < 1 || num > 1000) {
+ messagearea.text(conf.localization.noRepeatEvery).show();
+ return false;
+ }
+ }
+
+ // Repeats Weekly
+ if (self.$modalForm.find("#riweeklyinterval").css("display") === "block") {
+ // Check repeat every field
+ num = findIntField("riweeklyinterval", form);
+ if (!num || num < 1 || num > 1000) {
+ messagearea.text(conf.localization.noRepeatEvery).show();
+ return false;
+ }
+ }
+
+ // Repeats Monthly
+ if (self.$modalForm.find("#rimonthlyinterval").css("display") === "block") {
+ // Check repeat every field
+ num = findIntField("rimonthlyinterval", form);
+ if (!num || num < 1 || num > 1000) {
+ messagearea.text(conf.localization.noRepeatEvery).show();
+ return false;
+ }
+
+ // Check repeat on
+ if (self.$modalForm.find("#rimonthlyoptions input:checked").length === 0) {
+ messagearea.text(conf.localization.noRepeatOn).show();
+ return false;
+ }
+ }
+
+ // Repeats Yearly
+ if (self.$modalForm.find("#riyearlyinterval").css("display") === "block") {
+ // Check repeat every field
+ num = findIntField("riyearlyinterval", form);
+ if (!num || num < 1 || num > 1000) {
+ messagearea.text(conf.localization.noRepeatEvery).show();
+ return false;
+ }
+
+ // Check repeat on
+ if (self.$modalForm.find("#riyearlyoptions input:checked").length === 0) {
+ messagearea.text(conf.localization.noRepeatOn).show();
+ return false;
+ }
+ }
+
+ // End recurrence fields
+
+ // If after N occurences is selected, check its value
+ if (
+ self.$modalForm.find('input[value="BYOCCURRENCES"]:visible:checked').length >
+ 0
+ ) {
+ num = findIntField("rirangebyoccurrencesvalue", form);
+ if (!num || num < 1 || num > 1000) {
+ messagearea.text(conf.localization.noEndAfterNOccurrences).show();
+ return false;
+ }
+ }
+
+ // If end date is selected, check its value
+ if (
+ self.$modalForm.find('input[value="BYENDDATE"]:visible:checked').length > 0
+ ) {
+ endDate = findEndDate(form);
+ if (!endDate) {
+ // if endDate is null that means the field is empty
+ messagearea.text(conf.localization.noEndDate).show();
+ return false;
+ } else if (endDate < startDate) {
+ // the end date cannot be before start date
+ messagearea.text(conf.localization.pastEndDate).show();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function save(event) {
+ event.preventDefault();
+ // if no field errors, process the request
+ if (checkFields(self.$modalForm)) {
+ // close modal
+ self.modal.hide();
+ recurrenceOn(self.$modalForm);
+ self.$modalForm = null;
+ }
+ }
+
+ function cancel(event) {
+ event.preventDefault();
+ // close modal
+ self.modal.hide();
+ self.$modalForm = null;
+ }
+
+ function updateOccurrences() {
+ var startDate;
+ startDate = findStartDate();
+
+ // if no field errors, process the request
+ if (checkFields(form)) {
+ loadOccurrences(
+ startDate,
+ widgetSaveToRfc5545(
+ self.$modalForm,
+ textarea["ical"].RDATE,
+ textarea["ical"].EXDATE,
+ conf,
+ false,
+ ).result,
+ 0,
+ false,
+ );
+ }
+ }
+
+ function initModalForm() {
+ var enddate = self.$modalForm.find("input[name=rirangebyenddatecalendar]");
+ if (!enddate.val()) {
+ var start_date = findStartDate();
+ start_date.setDate(start_date.getDate() + 1);
+ enddate.val(start_date.toISOString().substring(0, 10));
+ }
+
+ self.$modalForm.find("input#addaction").on("click", occurrenceAdd);
+
+ // When selecting template, update what fieldsets are visible and the occurrences.
+ self.$modalForm.find('select[name="rirtemplate"]').on("change", function () {
+ displayFields($(this));
+ updateOccurrences();
+ });
+
+ // Update the selected dates section
+ self.$modalForm
+ .find(
+ `
+ input:radio,
+ input:checkbox,
+ .riweeklyweekday > input,
+ input[name=ridailyinterval],
+ input[name=riweeklyinterval],
+ input[name=rimonthlyinterval],
+ input[name=riyearlyinterval],
+ input[name=rirangebyoccurrencesvalue],
+ input[name=rirangebyenddatecalendar],
+ #rimonthlyoptions select,
+ #riyearlyoptions select,
+ #riyearlyinterval select`,
+ )
+ .on("change", function () {
+ // Update only if the occurances are shown
+ if (self.$modalForm.find(".rioccurrencesactions:visible").length !== 0) {
+ updateOccurrences();
+ }
+ });
+
+ // initialize occurrence adddate value
+ self.$modalForm.find("div.riaddoccurrence input#adddate").val(now_date);
+
+ /*
+ Save and cancel methods:
+ */
+ self.$modalForm.find(".ricancelbutton").on("click", cancel);
+ self.$modalForm.find(".risavebutton").on("click", save);
+ }
+
+ /*
+ Load the templates
+ */
+
+ self.display = $(_.template(DisplayTemplate)(conf));
+ var form = $(_.template(FormTemplate)(conf));
+
+ form.appendTo("body");
+ form.hide();
+
+ // Make an modal and hide it
+ self.modal = new Modal(form, {
+ content: "div.riform",
+ buttons: ".ributtons > button",
+ modalSizeClass: "modal-lg",
+ backdropOptions: {
+ opacity: "0.5",
+ },
+ });
+
+ if (textarea.innerHTML) {
+ var result = widgetLoadFromRfc5545(form, conf, textarea["ical"], false);
+ if (result === -1) {
+ var label = self.display.find("label[class=ridisplay-label]");
+ label.text(conf.localization.noRule);
+ } else {
+ recurrenceOn(form);
+ }
+ }
+
+ /*
+ Do all the GUI stuff:
+ */
+
+ // When you click "Delete...", the recurrence rules should be cleared.
+ self.display.find('button[name="ridelete"]').on("click", function (e) {
+ e.preventDefault();
+ recurrenceOff();
+ });
+
+ // Show form modal when you click on the "Edit..." link
+ self.display.find('button[name="riedit"]').on("click", function (e) {
+ // Load the form to set up the right fields to show, etc.
+ e.preventDefault();
+ self.modal.show();
+ self.$modalForm = $("form", self.modal.$modalContent);
+ loadData(self.$modalForm);
+ initModalForm();
+ });
+};
+
+// Pattern properties: grafted onto the registered thin pattern in recurrence.js.
+export default {
+ defaults: {
+ localization: {
+ displayUnactivate: "Does not repeat",
+ displayActivate: "Repeats every",
+ add_rules: "Add",
+ edit_rules: "Edit",
+ delete_rules: "Delete",
+ add: "Add",
+ refresh: "Refresh",
+
+ title: "Repeat",
+ preview: "Selected dates",
+ addDate: "Add date",
+
+ recurrenceType: "Repeats:",
+
+ dailyInterval1: "Repeat every:",
+ dailyInterval2: "days",
+
+ weeklyInterval1: "Repeat every:",
+ weeklyInterval2: "week(s)",
+ weeklyWeekdays: "Repeat on:",
+ weeklyWeekdaysHuman: "on:",
+
+ monthlyInterval1: "Repeat every:",
+ monthlyInterval2: "month(s)",
+ monthlyDayOfMonth1: "Day",
+ monthlyDayOfMonth1Human: "on day",
+ monthlyDayOfMonth2: "of the month",
+ monthlyDayOfMonth3: "month(s)",
+ monthlyWeekdayOfMonth1: "The",
+ monthlyWeekdayOfMonth1Human: "on the",
+ monthlyWeekdayOfMonth2: "",
+ monthlyWeekdayOfMonth3: "of the month",
+ monthlyRepeatOn: "Repeat on:",
+
+ yearlyInterval1: "Repeat every:",
+ yearlyInterval2: "year(s)",
+ yearlyDayOfMonth1: "Every",
+ yearlyDayOfMonth1Human: "on",
+ yearlyDayOfMonth2: "",
+ yearlyDayOfMonth3: "",
+ yearlyWeekdayOfMonth1: "The",
+ yearlyWeekdayOfMonth1Human: "on the",
+ yearlyWeekdayOfMonth2: "",
+ yearlyWeekdayOfMonth3: "of",
+ yearlyWeekdayOfMonth4: "",
+ yearlyRepeatOn: "Repeat on:",
+
+ range: "End recurrence:",
+ rangeNoEnd: "Never",
+ rangeByOccurrences1: "After",
+ rangeByOccurrences1Human: "ends after",
+ rangeByOccurrences2: "occurrence(s)",
+ rangeByEndDate: "On",
+ rangeByEndDateHuman: "ends on",
+
+ including: ", and also",
+ except: ", except for",
+
+ cancel: "Cancel",
+ save: "Save",
+
+ recurrenceStart: "Start of the recurrence",
+ additionalDate: "Additional date",
+ include: "Include",
+ exclude: "Exclude",
+ remove: "Remove",
+
+ orderIndexes: ["first", "second", "third", "fourth", "last"],
+ months: [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ],
+ shortMonths: [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ],
+ weekdays: [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ ],
+ shortWeekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
+
+ longDateFormat: "mmmm dd, yyyy",
+ shortDateFormat: "mm/dd/yyyy",
+
+ unsupportedFeatures:
+ "Warning: This event uses recurrence features not " +
+ "supported by this widget. Saving the recurrence " +
+ "may change the recurrence in unintended ways:",
+ noTemplateMatch: "No matching recurrence template",
+ multipleDayOfMonth:
+ "This widget does not support multiple days in monthly or yearly recurrence",
+ bysetpos: "BYSETPOS is not supported",
+ noRule: "No RRULE in RRULE data",
+ noRepeatEvery: 'Error: The "Repeat every"-field must be between 1 and 1000',
+ noEndDate: "Error: End date is not set. Please set a correct value",
+ noRepeatOn: 'Error: "Repeat on"-value must be selected',
+ pastEndDate: "Error: End date cannot be before start date",
+ noEndAfterNOccurrences:
+ 'Error: The "After N occurrences"-field must be between 1 and 1000',
+ alreadyAdded: "This date was already added",
+
+ rtemplate: {
+ daily: "Daily",
+ mondayfriday: "Monday and Friday",
+ weekdays: "Weekday",
+ weekly: "Weekly",
+ monthly: "Monthly",
+ yearly: "Yearly",
+ },
+
+ error_load_occurrences: "Cannot load the occurrences preview.",
+ },
+
+ readOnly: false,
+ firstDay: 0,
+
+ // "REMOTE" FIELD
+ startField: null,
+ startFieldYear: null,
+ startFieldMonth: null,
+ startFieldDay: null,
+ ajaxURL: null,
+ ajaxContentType: "application/json; charset=utf8",
+ ributtonExtraClass: "",
+
+ // INPUT CONFIGURATION
+ allowAdditionalDates: false,
+ hasRepeatForeverButton: true,
+
+ // JQUERY TEMPLATE NAMES
+ template: {
+ form: "#jquery-recurrenceinput-form-tmpl",
+ display: "#jquery-recurrenceinput-display-tmpl",
+ },
+
+ // RECURRENCE TEMPLATES
+ rtemplate: {
+ daily: {
+ rrule: "FREQ=DAILY",
+ fields: ["ridailyinterval", "rirangeoptions"],
+ },
+ mondayfriday: {
+ rrule: "FREQ=WEEKLY;BYDAY=MO,FR",
+ fields: ["rirangeoptions"],
+ },
+ weekdays: {
+ rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
+ fields: ["rirangeoptions"],
+ },
+ weekly: {
+ rrule: "FREQ=WEEKLY",
+ fields: ["riweeklyinterval", "riweeklyweekdays", "rirangeoptions"],
+ },
+ monthly: {
+ rrule: "FREQ=MONTHLY",
+ fields: ["rimonthlyinterval", "rimonthlyoptions", "rirangeoptions"],
+ },
+ yearly: {
+ rrule: "FREQ=YEARLY",
+ fields: ["riyearlyinterval", "riyearlyoptions", "rirangeoptions"],
+ },
+ },
+ },
+
+ load_icons: async function () {
+ return {
+ reload: await utils.resolveIcon("arrow-clockwise"),
+ remove: await utils.resolveIcon("trash"),
+ exclude: await utils.resolveIcon("calendar-x"),
+ include: await utils.resolveIcon("plus-circle"),
+ };
+ },
+
+ init: async function () {
+ // tmpl BEFORE recurrenceinput
+ import("./recurrence.scss");
+
+ this.el.classList.add("recurrence-widget");
+ var icons = await this.load_icons();
+
+ var config = {
+ ...this.options,
+ ...this.options.configuration,
+ icons: icons,
+ };
+
+ // our recurrenceinput widget instance
+ var recurrenceinput = new RecurrenceInput(config, this.el);
+ // hide textarea and place display widget after textarea
+ this.$el.after(recurrenceinput.display);
+
+ // hide the textarea
+ this.el.style.display = "none";
+ },
+};
diff --git a/src/pat/recurrence/recurrence.js b/src/pat/recurrence/recurrence.js
index 04746548ce..19f02174eb 100644
--- a/src/pat/recurrence/recurrence.js
+++ b/src/pat/recurrence/recurrence.js
@@ -1,1466 +1,21 @@
import $ from "jquery";
-import _ from "underscore";
import Base from "@patternslib/patternslib/src/core/base";
-import DisplayTemplate from "./templates/display.xml";
-import FormTemplate from "./templates/form.xml";
-import OccurrenceTemplate from "./templates/occurrence.xml";
-import Modal from "../modal/modal";
-import utils from "../../core/utils";
-
-// Formatting function (mostly) from jQueryTools dateinput
-var Re = /d{1,4}|m{1,4}|yy(?:yy)?|"[^"]*"|'[^']*'/g;
-
-function zeropad(val, len) {
- val = val.toString();
- len = len || 2;
- while (val.length < len) {
- val = "0" + val;
- }
- return val;
-}
-
-function format(date, fmt, conf) {
- var d = date.getDate(),
- D = date.getDay(),
- m = date.getMonth(),
- y = date.getFullYear(),
- flags = {
- d: d,
- dd: zeropad(d),
- ddd: conf.localization.shortWeekdays[D],
- dddd: conf.localization.weekdays[D],
- m: m + 1,
- mm: zeropad(m + 1),
- mmm: conf.localization.shortMonths[m],
- mmmm: conf.localization.months[m],
- yy: String(y).slice(2),
- yyyy: y,
- };
-
- var result = fmt.replace(Re, function ($0) {
- return Object.prototype.hasOwnProperty.call(flags, $0)
- ? flags[$0]
- : $0.slice(1, $0.length - 1);
- });
-
- return result;
-}
-
-function widgetSaveToRfc5545(form, RDATE, EXDATE, conf, tz) {
- var value = form.find("select[name=rirtemplate]").val();
- var rtemplate = conf.rtemplate[value];
- var result = "RRULE:" + rtemplate.rrule;
- var human = conf.localization.rtemplate[value];
- var interval, month, year, occurrences, day;
- var weekdays, i18nweekdays, input, monthlyType, index;
- var yearlyType, rangeType;
-
- for (const field_id of rtemplate.fields) {
- const field = form.find(`#${field_id}`);
-
- switch (field.attr("id")) {
- case "ridailyinterval":
- interval = field.find("input[name=ridailyinterval]").val();
- if (interval !== "1") {
- result += `;INTERVAL=${interval}`;
- }
- human = `${interval} ${conf.localization.dailyInterval2}`;
- break;
-
- case "riweeklyinterval":
- interval = field.find("input[name=riweeklyinterval]").val();
- if (interval !== "1") {
- result += `;INTERVAL=${interval}`;
- }
- human = `${interval} ${conf.localization.weeklyInterval2}`;
- break;
-
- case "riweeklyweekdays":
- weekdays = [];
- i18nweekdays = [];
- for (let j = 0; j < conf.weekdays.length; j++) {
- input = field.find(`input[name=riweeklyweekdays${j}]`);
- if (input.is(":checked")) {
- weekdays.push(conf.weekdays[j]);
- i18nweekdays.push(conf.localization.weekdays[j]);
- }
- }
- if (weekdays.length > 0) {
- result += `;BYDAY=${weekdays.join(",")}`;
- human += ` ${
- conf.localization.weeklyWeekdaysHuman
- } ${i18nweekdays.join(",")}`;
- }
- break;
-
- case "rimonthlyinterval":
- interval = field.find("input[name=rimonthlyinterval]").val();
- if (interval !== "1") {
- result += `;INTERVAL=${interval}`;
- }
- human = `${interval} ${conf.localization.monthlyInterval2}`;
- break;
-
- case "rimonthlyoptions":
- monthlyType = $("input[name=rimonthlytype]:checked", form).val();
- if (monthlyType === "DAYOFMONTH") {
- day = $("select[name=rimonthlydayofmonthday]", form).val();
- result += `;BYMONTHDAY=${day}`;
- human += `, ${conf.localization.monthlyDayOfMonth1Human} ${day} ${conf.localization.monthlyDayOfMonth2}`;
- } else if (monthlyType === "WEEKDAYOFMONTH") {
- index = $("select[name=rimonthlyweekdayofmonthindex]", form).val();
- day =
- conf.weekdays[
- $("select[name=rimonthlyweekdayofmonth]", form).val()
- ];
- if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) {
- result += `;BYDAY=${index}${day}`;
- human += `, ${conf.localization.monthlyWeekdayOfMonth1Human} `;
- human += `${
- conf.localization.orderIndexes[
- conf.orderIndexes.indexOf(index)
- ]
- } ${conf.localization.monthlyWeekdayOfMonth2} `;
- human += `${
- conf.localization.weekdays[conf.weekdays.indexOf(day)]
- } ${conf.localization.monthlyDayOfMonth2}`;
- }
- }
- break;
-
- case "riyearlyinterval":
- interval = field.find("input[name=riyearlyinterval]").val();
- if (interval !== "1") {
- result += `;INTERVAL=${interval}`;
- }
- human = `${interval} ${conf.localization.yearlyInterval2}`;
- break;
-
- case "riyearlyoptions":
- yearlyType = $("input[name=riyearlyType]:checked", form).val();
-
- if (yearlyType === "DAYOFMONTH") {
- month = $("select[name=riyearlydayofmonthmonth]", form).val();
- day = $("select[name=riyearlydayofmonthday]", form).val();
- result += `;BYMONTH=${month};BYMONTHDAY=${day}`;
- human += `, ${conf.localization.yearlyDayOfMonth1Human} ${
- conf.localization.months[month - 1]
- } ${day}`;
- } else if (yearlyType === "WEEKDAYOFMONTH") {
- index = $("select[name=riyearlyweekdayofmonthindex]", form).val();
- day =
- conf.weekdays[
- $("select[name=riyearlyweekdayofmonthday]", form).val()
- ];
- month = $("select[name=riyearlyweekdayofmonthmonth]", form).val();
- result += `;BYMONTH=${month}`;
- if (["MO", "TU", "WE", "TH", "FR", "SA", "SU"].includes(day)) {
- result += `;BYDAY=${index}${day}`;
- human += ", " + conf.localization.yearlyWeekdayOfMonth1Human;
- human +=
- " " +
- conf.localization.orderIndexes[
- $.inArray(index, conf.orderIndexes)
- ];
- human += " " + conf.localization.yearlyWeekdayOfMonth2;
- human +=
- " " +
- conf.localization.weekdays[$.inArray(day, conf.weekdays)];
- human += " " + conf.localization.yearlyWeekdayOfMonth3;
- human += " " + conf.localization.months[month - 1];
- human += " " + conf.localization.yearlyWeekdayOfMonth4;
- }
- }
- break;
-
- case "rirangeoptions":
- rangeType = form.find("input[name=rirangetype]:checked").val();
- if (rangeType === "BYOCCURRENCES") {
- occurrences = form
- .find("input[name=rirangebyoccurrencesvalue]")
- .val();
- result += `;COUNT=${occurrences}`;
- human += `, ${conf.localization.rangeByOccurrences1Human} ${occurrences} ${conf.localization.rangeByOccurrences2}`;
- } else if (rangeType === "BYENDDATE") {
- let date = form.find("input[name=rirangebyenddatecalendar]").val();
- if (date === "") {
- const today = new Date();
- date = `${today.getFullYear()}-${(today.getMonth() + 1)
- .toString()
- .padStart(2, "0")}-${today.getDate()}`;
- }
- result += `;UNTIL=${date.replaceAll("-", "")}T000000${
- tz === true ? "Z" : ""
- }`;
- human += `, ${conf.localization.rangeByEndDateHuman} `;
- var date_parts = date.split("-");
- human += format(
- new Date(date_parts[0], date_parts[1] - 1, date_parts[2]),
- conf.localization.longDateFormat,
- conf,
- );
- }
- break;
- }
- }
-
- if (RDATE.length) {
- RDATE.sort();
- let tmp_dates = [];
- let tmp_human = [];
-
- // make sure our additional RDATE dates have the same start time
- // XXX: not used, remove if superfluous
- //const rdate_time = start_date
- // ? `T${start_date.getHours()}:${start_date.getMinutes()}:00`
- // : "T00:00:00";
-
- for (let rdate of RDATE) {
- if (rdate !== "") {
- // The values from the input field are ISO8601 "YYYY-MM-DD"
- // RFC5545 expects a date or date-time format of e.g.
- // "YYYYMMDD". See:
- // https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.2
- rdate = rdate.replaceAll("-", "");
- // by adding "T000000" the recurrence sequence generator of
- // plone.event.recurrence adds the current start time correctly
- rdate += rdate.length === 8 ? "T000000" : "";
- rdate += tz ? "Z" : "";
- tmp_dates.push(rdate);
-
- // human readable RDATE
- year = parseInt(rdate.substring(0, 4), 10);
- month = parseInt(rdate.substring(4, 6), 10) - 1; // js month.
- day = parseInt(rdate.substring(6, 8), 10);
- tmp_human.push(
- format(
- new Date(year, month, day),
- conf.localization.longDateFormat,
- conf,
- ),
- );
- }
- }
- if (tmp_dates.length) {
- result += `\nRDATE:${tmp_dates.join(",")}`;
- human += `${conf.localization.including} ${tmp_human.join("; ")}`;
- }
- }
-
- if (EXDATE.length) {
- EXDATE.sort();
- let tmp_dates = [];
- let tmp_human = [];
- for (let exdate of EXDATE) {
- if (exdate !== "") {
- // EXDATE values are "YYYYMMDDTHHMMZ"
- tmp_dates.push(exdate);
-
- // human readable EXDATE
- year = parseInt(exdate.substring(0, 4), 10);
- month = parseInt(exdate.substring(4, 6), 10) - 1;
- day = parseInt(exdate.substring(6, 8), 10);
- tmp_human.push(
- format(
- new Date(year, month, day),
- conf.localization.longDateFormat,
- conf,
- ),
- );
- }
- }
- if (tmp_dates.length) {
- result += `\nEXDATE:${tmp_dates.join(",")}`;
- human += `${conf.localization.except} ${tmp_human.join("; ")}`;
- }
- }
-
- return { result: result, description: human };
-}
-
-function parseLine(icalline) {
- var result = {};
- var pos = icalline.indexOf(":");
- var property = icalline.substring(0, pos);
- result.value = icalline.substring(pos + 1);
-
- if (property.indexOf(";") !== -1) {
- pos = property.indexOf(";");
- result.parameters = property.substring(pos + 1);
- result.property = property.substring(0, pos);
- } else {
- result.parameters = null;
- result.property = property;
- }
- return result;
-}
-
-function cleanDates(dates) {
- // Get rid of timezones
- // TODO: We could parse dates and range here, maybe?
- var result = [];
-
- for (const date of dates.split(",")) {
- if (date.indexOf("Z") !== -1) {
- result.push(date.substring(0, 15));
- } else {
- result.push(date);
- }
- }
- return result;
-}
-
-function parseIcal(icaldata) {
- var result = {
- RRULE: "",
- RDATE: [],
- EXDATE: [],
- };
- var line = null;
- var nextline;
-
- var lines = icaldata.split("\n");
- lines.reverse();
- while (line !== "") {
- if (lines.length > 0) {
- nextline = lines.pop();
- if (nextline.charAt(0) === " " || nextline.charAt(0) === "\t") {
- // Line continuation:
- line = line + nextline;
- continue;
- }
- } else {
- nextline = "";
- }
-
- // New line; the current one is finished, add it to the result.
- if (line !== null) {
- line = parseLine(line);
- // We ignore properties for now
- if (line.property === "RDATE" || line.property === "EXDATE") {
- result[line.property] = cleanDates(line.value);
- } else {
- result[line.property] = line.value;
- }
- }
-
- line = nextline;
- }
- return result;
-}
-
-function widgetLoadFromRfc5545(form, conf, icaldata, force) {
- var unsupportedFeatures = [];
- var matches, rtemplate, d, input, index;
- var selectors, field, radiobutton;
- var freq, interval, byday, bymonth, bymonthday, count, until;
- var day, month, year, weekday;
-
- if (icaldata.RRULE === undefined) {
- unsupportedFeatures.push(conf.localization.noRule);
- if (!force) {
- return -1; // Fail!
- }
- } else {
- matches = /FREQ=([^;]+);?/.exec(icaldata.RRULE);
- if (matches) {
- freq = matches[1];
- } else {
- freq = "DAILY";
- }
-
- matches = /INTERVAL=([0-9]+);?/.exec(icaldata.RRULE);
- if (matches) {
- interval = matches[1];
- } else {
- interval = "1";
- }
-
- matches = /BYDAY=([^;]+);?/.exec(icaldata.RRULE);
- if (matches) {
- byday = matches[1];
- } else {
- byday = "";
- }
-
- matches = /BYMONTHDAY=([^;]+);?/.exec(icaldata.RRULE);
- if (matches) {
- bymonthday = matches[1].split(",");
- } else {
- bymonthday = null;
- }
-
- matches = /BYMONTH=([^;]+);?/.exec(icaldata.RRULE);
- if (matches) {
- bymonth = matches[1].split(",");
- } else {
- bymonth = null;
- }
-
- matches = /COUNT=([0-9]+);?/.exec(icaldata.RRULE);
- if (matches) {
- count = matches[1];
- } else {
- count = null;
- }
-
- matches = /UNTIL=([0-9T]+);?/.exec(icaldata.RRULE);
- if (matches) {
- until = matches[1];
- } else {
- until = null;
- }
-
- matches = /BYSETPOS=([^;]+);?/.exec(icaldata.RRULE);
- if (matches) {
- unsupportedFeatures.push(conf.localization.bysetpos);
- }
-
- // Find the best rule:
- if (Object.prototype.hasOwnProperty.call(conf.rtemplate, freq.toLowerCase())) {
- rtemplate = conf.rtemplate[freq.toLowerCase()];
- } else {
- for (freq of conf.rtemplate) {
- /* fallback to first available template if no match above*/
- rtemplate = conf.rtemplate[freq];
- break;
- }
- unsupportedFeatures.push(conf.localization.noTemplateMatch);
- }
-
- // set rirtemplate selector to computed value
- form.find("select[name='rirtemplate']").val(freq.toLowerCase());
-
- for (const field_id of rtemplate.fields) {
- field = form.find(`#${field_id}`);
- switch (field.attr("id")) {
- case "ridailyinterval":
- field.find("input[name=ridailyinterval]").val(interval);
- break;
-
- case "riweeklyinterval":
- field.find("input[name=riweeklyinterval]").val(interval);
- break;
-
- case "riweeklyweekdays":
- if (byday.length === 0) break;
- byday = byday.split(",");
- for (d = 0; d < conf.weekdays.length; d++) {
- input = field.find(`input[name="riweeklyweekdays${d}"]`);
- if (input.length === 0) continue;
- day = conf.weekdays[d];
- input.attr("checked", byday.includes(day));
- }
- break;
-
- case "rimonthlyinterval":
- field.find("input[name=rimonthlyinterval]").val(interval);
- break;
-
- case "rimonthlyoptions":
- var monthlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY
-
- if (bymonthday) {
- monthlyType = "DAYOFMONTH";
- if (bymonthday.length > 1) {
- // No support for multiple days in one month
- unsupportedFeatures.push(
- conf.localization.multipleDayOfMonth,
- );
- // Just keep the first
- bymonthday = bymonthday[0];
- }
- field
- .find("select[name=rimonthlydayofmonthday]")
- .val(bymonthday);
- }
-
- if (byday) {
- monthlyType = "WEEKDAYOFMONTH";
-
- if (byday.indexOf(",") !== -1) {
- // No support for multiple days in one month
- unsupportedFeatures.push(
- conf.localization.multipleDayOfMonth,
- );
- byday = byday.split(",")[0];
- }
- index = byday.slice(0, -2);
- if (index.charAt(0) !== "+" && index.charAt(0) !== "-") {
- index = "+" + index;
- }
- weekday = byday.slice(-2);
- field
- .find("select[name=rimonthlyweekdayofmonthindex]")
- .val(index);
- const day_index = conf.weekdays.indexOf(weekday);
- field
- .find("select[name=rimonthlyweekdayofmonth]")
- .val(day_index);
- }
-
- selectors = field.find("input[name=rimonthlytype]");
- for (index = 0; index < selectors.length; index++) {
- radiobutton = selectors[index];
- $(radiobutton).attr(
- "checked",
- radiobutton.value === monthlyType,
- );
- }
- break;
-
- case "riyearlyinterval":
- field.find("input[name=riyearlyinterval]").val(interval);
- break;
-
- case "riyearlyoptions":
- var yearlyType = "DAYOFMONTH"; // Default to using BYMONTHDAY
-
- if (bymonthday) {
- yearlyType = "DAYOFMONTH";
- if (bymonthday.length > 1) {
- // No support for multiple days in one month
- unsupportedFeatures.push(
- conf.localization.multipleDayOfMonth,
- );
- bymonthday = bymonthday[0];
- }
- field.find("select[name=riyearlydayofmonthmonth]").val(bymonth);
- field.find("select[name=riyearlydayofmonthday]").val(bymonthday);
- }
-
- if (byday) {
- yearlyType = "WEEKDAYOFMONTH";
-
- if (byday.indexOf(",") !== -1) {
- // No support for multiple days in one month
- unsupportedFeatures.push(
- conf.localization.multipleDayOfMonth,
- );
- byday = byday.split(",")[0];
- }
- index = byday.slice(0, -2);
- if (index.charAt(0) !== "+" && index.charAt(0) !== "-") {
- index = "+" + index;
- }
- weekday = byday.slice(-2);
- field
- .find("select[name=riyearlyweekdayofmonthindex]")
- .val(index);
-
- const weekday_index = conf.weekdays.indexOf(weekday);
- field
- .find("select[name=riyearlyweekdayofmonthday]")
- .val(weekday_index);
- field
- .find("select[name=riyearlyweekdayofmonthmonth]")
- .val(bymonth);
- }
-
- selectors = field.find("input[name=riyearlyType]");
- for (index = 0; index < selectors.length; index++) {
- radiobutton = selectors[index];
- $(radiobutton).attr("checked", radiobutton.value === yearlyType);
- }
- break;
-
- case "rirangeoptions":
- // default value per configuration
- var rangeType = conf.hasRepeatForeverButton
- ? "NOENDDATE"
- : "BYOCCURRENCES";
-
- if (count) {
- rangeType = "BYOCCURRENCES";
- field.find("input[name=rirangebyoccurrencesvalue]").val(count);
- }
-
- if (until) {
- rangeType = "BYENDDATE";
- input = field.find("input[name=rirangebyenddatecalendar]");
- year = until.slice(0, 4);
- month = until.slice(4, 6);
- day = until.slice(6, 8);
- input.val(`${year}-${month}-${day}`);
- }
-
- selectors = field.find("input[name=rirangetype]");
- for (index = 0; index < selectors.length; index++) {
- radiobutton = selectors[index];
- $(radiobutton).attr("checked", radiobutton.value === rangeType);
- }
- break;
- }
- }
- }
-
- var messagearea = form.find("#messagearea");
- if (unsupportedFeatures.length !== 0) {
- messagearea.text(
- conf.localization.unsupportedFeatures + " " + unsupportedFeatures.join("; "),
- );
- messagearea.show();
- return 1;
- } else {
- messagearea.text("");
- messagearea.hide();
- return 0;
- }
-}
-
-const RecurrenceInput = function (conf, textarea) {
- var self = this;
- var $textarea = $(textarea);
-
- // initalize parsed icaldata
- if (textarea.innerHTML) {
- textarea["ical"] = parseIcal(textarea.innerHTML);
- } else {
- textarea["ical"] = {
- RRULE: "",
- RDATE: [],
- EXDATE: [],
- };
- }
-
- // Extend conf with non-configurable data used by templates.
- var orderedWeekdays = [];
- var now_date = new Date().toISOString().substring(0, 10);
- var index;
-
- for (let i = 0; i < 7; i++) {
- index = i + conf.firstDay;
- if (index > 6) {
- index = index - 7;
- }
- orderedWeekdays.push(index);
- }
-
- conf = {
- ...conf,
- orderIndexes: ["+1", "+2", "+3", "+4", "-1"],
- weekdays: ["SU", "MO", "TU", "WE", "TH", "FR", "SA"],
- orderedWeekdays: orderedWeekdays,
- };
-
- // The recurrence type dropdown should show certain fields depending
- // on selection:
- function displayFields(selector) {
- // First hide all the fields
- self.$modalForm.find(".rifield").hide();
- // Then show the ones that should be shown.
- var value = selector.val();
- if (value) {
- for (const rtField of conf.rtemplate[value].fields) {
- self.$modalForm.find(`#${rtField}`).show();
- }
- }
- }
-
- function occurrenceExclude(event) {
- event.preventDefault();
- textarea["ical"].EXDATE.push(this.attributes.date.value);
- var $this = $(this);
- $this.addClass("exdate");
- $this.parent().parent().addClass("exdate");
- $this.off("click").on("click", occurrenceInclude);
- }
-
- function occurrenceInclude(event) {
- event.preventDefault();
- textarea["ical"].EXDATE.splice(
- $.inArray(this.attributes.date.value, textarea["ical"].EXDATE),
- 1,
- );
- var $this = $(this);
- $this.removeClass("exdate");
- $this.parent().parent().removeClass("exdate");
- $this.off("click").on("click", occurrenceExclude);
- }
-
- function occurrenceDelete(event) {
- event.preventDefault();
- textarea["ical"].RDATE.splice(
- $.inArray(this.attributes.date.value, textarea["ical"].RDATE),
- 1,
- );
- $(this)
- .parent()
- .parent()
- .hide("slow", function () {
- $(this).remove();
- });
- }
-
- function occurrenceAdd(event) {
- event.preventDefault();
- var datevalue = self.$modalForm.find(".riaddoccurrence input#adddate").val();
- var date_parts = datevalue.split("-");
- datevalue += datevalue.length === 10 ? "T000000" : "";
- var errorarea = self.$modalForm.find(".riaddoccurrence div.alert");
- errorarea.text("");
- errorarea.hide();
-
- // Add date only if it is not already in RDATE
- if (!textarea["ical"].RDATE.includes(datevalue)) {
- textarea["ical"].RDATE.push(datevalue);
- var $newdate =
- $(`
-
- ${format(
- new Date(date_parts[0], date_parts[1] - 1, date_parts[2]),
- conf.localization.longDateFormat,
- conf,
- )},
- ${conf.localization.additionalDate}
-
-
-
-
-
`);
- $newdate.hide();
- self.$modalForm.find("div.rioccurrences").prepend($newdate);
- $newdate.slideDown();
- $newdate.find("button.rdate").on("click", occurrenceDelete);
- } else {
- errorarea.text(conf.localization.alreadyAdded).show();
- }
- }
-
- // element is where to find the tag in question. Can be the form
- // or the display widget. Defaults to the self.$modalForm.
- function loadOccurrences(startdate, rfc5545, start, readonly) {
- var element, occurrenceDiv;
-
- if (!readonly) {
- element = self.$modalForm;
- } else {
- element = self.display;
- }
-
- occurrenceDiv = element.find(".rioccurrences");
-
- const year = startdate.getFullYear();
- const month = startdate.getMonth() + 1;
- const day = startdate.getDate();
-
- var data = {
- year: year,
- month: month, // Sending January as 0? I think not.
- day: day,
- rrule: rfc5545,
- format: conf.localization.longDateFormat,
- start: start,
- };
-
- $.ajax({
- url: conf.ajaxURL,
- async: false, // Can't be tested if it's asynchronous, annoyingly.
- type: "post",
- dataType: "json",
- contentType: conf.ajaxContentType,
- cache: false,
- data: data,
- success: function (resp) {
- var result;
-
- resp.readOnly = readonly;
- resp.localization = conf.localization;
- resp.icons = conf.icons;
-
- // Format dates:
- var date, y, m, d;
- for (let occurrence of resp.occurrences) {
- date = occurrence.date;
- y = parseInt(date.substring(0, 4), 10);
- m = parseInt(date.substring(4, 6), 10) - 1; // jan=0
- d = parseInt(date.substring(6, 8), 10);
- occurrence.date = `${y}-${zeropad(m + 1)}-${zeropad(d)}T000000`;
- occurrence.formattedDate = format(
- new Date(y, m, d),
- conf.localization.longDateFormat,
- conf,
- );
- }
-
- result = _.template(OccurrenceTemplate)(resp);
- occurrenceDiv.replaceWith(result);
-
- // Add the batch actions:
- element.find(".rioccurrences .batching a").on("click", function (event) {
- event.stopPropagation();
- event.preventDefault();
- loadOccurrences(
- startdate,
- rfc5545,
- this.attributes.start.value,
- readonly,
- );
- });
-
- // Add the delete/undelete actions:
- if (!readonly) {
- element
- .find(".rioccurrences .action button.rrule")
- .on("click", occurrenceExclude);
- element
- .find(".rioccurrences .action button.exdate")
- .on("click", occurrenceInclude);
- element
- .find(".rioccurrences .action button.rdate")
- .on("click", occurrenceDelete);
- }
- },
- error: function () {
- occurrenceDiv[0].innerHTML = `
- ${conf.localization.error_load_occurrences}
- `;
- },
- });
- }
-
- function getField(field) {
- // See if it is a field already
- var realField = $(field);
- if (!realField.length) {
- // Otherwise, we assume it's an id:
- realField = $("#" + field);
- }
- if (!realField.length) {
- // Still not? Then it's a name.
- realField = $("input[name='" + field + "']");
- }
- return realField;
- }
- function findStartDate() {
- var startdate = null;
- var startField, startFieldYear, startFieldMonth, startFieldDay;
-
- // Find the default byday and bymonthday from the start date, if any:
- if (conf.startField) {
- startField = getField(conf.startField);
- if (!startField.length) {
- // Field not found
- return null;
- }
- startdate = startField.val();
- if (startdate === "") {
- // Probably not an input at all. Try to see if it contains a date
- startdate = startField.text();
- }
-
- if (typeof startdate === "string") {
- // convert human readable, non ISO8601 dates, like
- // '2014-04-24 19:00', where the 'T' separator is missing.
- startdate = startdate.replace(" ", "T");
- }
-
- startdate = new Date(startdate);
- } else if (conf.startFieldYear && conf.startFieldMonth && conf.startFieldDay) {
- startFieldYear = getField(conf.startFieldYear);
- startFieldMonth = getField(conf.startFieldMonth);
- startFieldDay = getField(conf.startFieldDay);
- if (
- !startFieldYear.length &&
- !startFieldMonth.length &&
- !startFieldDay.length
- ) {
- // Field not found
- return null;
- }
- startdate = new Date(
- startFieldYear.val(),
- startFieldMonth.val() - 1,
- startFieldDay.val(),
- );
- }
- if (startdate === null) {
- return null;
- }
- // We have some sort of startdate:
- if (isNaN(startdate)) {
- return null;
- }
- return startdate;
- }
- function findEndDate() {
- var endField, enddate;
-
- endField = self.$modalForm.find("input[name=rirangebyenddatecalendar]");
- enddate = endField.val();
- enddate = new Date(enddate);
-
- // if the end date is incorrect or the field is left empty
- if (isNaN(enddate) || endField.val() === "") {
- return null;
- }
- return enddate;
- }
- function findIntField(fieldName) {
- var field, num;
-
- field = self.$modalForm.find("input[name=" + fieldName + "]");
-
- num = field.val();
-
- // if it's not a number or the field is left empty
- if (isNaN(num) || num.toString().indexOf(".") !== -1 || field.val() === "") {
- return null;
- }
- return num;
- }
-
- // Loading (populating) display and form widget with
- // passed RFC5545 string (data)
- function loadData(form) {
- widgetLoadFromRfc5545(form, conf, textarea["ical"], true);
-
- const startdate = findStartDate();
-
- if (startdate !== null) {
- // If the date is a real date, set the defaults in the form
- document.querySelector("select[name=rimonthlydayofmonthday]").value =
- startdate.getDate();
- const dayindex =
- conf.orderIndexes[Math.floor((startdate.getDate() - 1) / 7)];
- const day = conf.weekdays[startdate.getDay()];
- document.querySelector("select[name=rimonthlyweekdayofmonthindex]").value =
- dayindex;
- document.querySelector("select[name=rimonthlyweekdayofmonth]").value = day;
-
- document.querySelector("select[name=riyearlydayofmonthmonth]").value =
- startdate.getMonth() + 1;
- document.querySelector("select[name=riyearlydayofmonthday]").value =
- startdate.getDate();
- document.querySelector("select[name=riyearlyweekdayofmonthindex]").value =
- dayindex;
- document.querySelector("select[name=riyearlyweekdayofmonthday]").value = day;
- document.querySelector("select[name=riyearlyweekdayofmonthmonth]").value =
- startdate.getMonth() + 1;
-
- // Now when we have a start date, we can also do an ajax call to calculate occurrences:
- var rfc5545 =
- textarea.innerHTML ||
- widgetSaveToRfc5545(
- form,
- textarea["ical"].RDATE,
- textarea["ical"].EXDATE,
- conf,
- false,
- ).result;
-
- loadOccurrences(startdate, rfc5545, 0, false);
-
- // Show the add and refresh buttons:
- document.querySelector("div.rioccurrencesactions").style.display = "block";
- } else {
- // No EXDATE/RDATE support
- document.querySelector("div.rioccurrencesactions").style.display = "none";
- }
-
- displayFields(form.find("select[name=rirtemplate]"));
- }
-
- function recurrenceOn(form) {
- var RFC5545 = widgetSaveToRfc5545(
- form,
- textarea["ical"].RDATE,
- textarea["ical"].EXDATE,
- conf,
- false,
- );
- var label = self.display.find("label[class=ridisplay-label]");
- label.text(conf.localization.displayActivate + " " + RFC5545.description);
- textarea["ical"] = parseIcal(RFC5545.result);
- textarea.innerHTML = RFC5545.result;
- $textarea.trigger("change");
- var startdate = findStartDate();
- if (startdate !== null) {
- loadOccurrences(startdate, RFC5545.result, 0, true);
- }
- self.display.find('button[name="riedit"]').text(conf.localization.edit_rules);
- self.display.find('button[name="ridelete"]').show();
- }
-
- function recurrenceOff() {
- var label = self.display.find("label[class=ridisplay-label]");
- label.text(conf.localization.displayUnactivate);
- // reset ical object
- textarea["ical"] = {
- RRULE: "",
- RDATE: [],
- EXDATE: [],
- };
- textarea.innerHTML = "";
- $textarea.trigger("change"); // Clear the textarea.
- self.display.find(".rioccurrences").hide();
- self.display.find('button[name="riedit"]').text(conf.localization.add_rules);
- self.display.find('button[name="ridelete"]').hide();
- }
-
- function checkFields(form) {
- var startDate, endDate, num, messagearea;
- startDate = findStartDate();
-
- // Hide any error message from before
- messagearea = self.$modalForm.find("#messagearea");
- messagearea.text("");
- messagearea.hide();
-
- // Hide add field errors
- self.$modalForm.find(".riaddoccurrence div.alert").text("").hide();
-
- // Repeats Daily
- if (self.$modalForm.find("#ridailyinterval").css("display") === "block") {
- // Check repeat every field
- num = findIntField("ridailyinterval", form);
- if (!num || num < 1 || num > 1000) {
- messagearea.text(conf.localization.noRepeatEvery).show();
- return false;
- }
- }
-
- // Repeats Weekly
- if (self.$modalForm.find("#riweeklyinterval").css("display") === "block") {
- // Check repeat every field
- num = findIntField("riweeklyinterval", form);
- if (!num || num < 1 || num > 1000) {
- messagearea.text(conf.localization.noRepeatEvery).show();
- return false;
- }
- }
-
- // Repeats Monthly
- if (self.$modalForm.find("#rimonthlyinterval").css("display") === "block") {
- // Check repeat every field
- num = findIntField("rimonthlyinterval", form);
- if (!num || num < 1 || num > 1000) {
- messagearea.text(conf.localization.noRepeatEvery).show();
- return false;
- }
-
- // Check repeat on
- if (self.$modalForm.find("#rimonthlyoptions input:checked").length === 0) {
- messagearea.text(conf.localization.noRepeatOn).show();
- return false;
- }
- }
-
- // Repeats Yearly
- if (self.$modalForm.find("#riyearlyinterval").css("display") === "block") {
- // Check repeat every field
- num = findIntField("riyearlyinterval", form);
- if (!num || num < 1 || num > 1000) {
- messagearea.text(conf.localization.noRepeatEvery).show();
- return false;
- }
-
- // Check repeat on
- if (self.$modalForm.find("#riyearlyoptions input:checked").length === 0) {
- messagearea.text(conf.localization.noRepeatOn).show();
- return false;
- }
- }
-
- // End recurrence fields
-
- // If after N occurences is selected, check its value
- if (
- self.$modalForm.find('input[value="BYOCCURRENCES"]:visible:checked').length >
- 0
- ) {
- num = findIntField("rirangebyoccurrencesvalue", form);
- if (!num || num < 1 || num > 1000) {
- messagearea.text(conf.localization.noEndAfterNOccurrences).show();
- return false;
- }
- }
-
- // If end date is selected, check its value
- if (
- self.$modalForm.find('input[value="BYENDDATE"]:visible:checked').length > 0
- ) {
- endDate = findEndDate(form);
- if (!endDate) {
- // if endDate is null that means the field is empty
- messagearea.text(conf.localization.noEndDate).show();
- return false;
- } else if (endDate < startDate) {
- // the end date cannot be before start date
- messagearea.text(conf.localization.pastEndDate).show();
- return false;
- }
- }
-
- return true;
- }
-
- function save(event) {
- event.preventDefault();
- // if no field errors, process the request
- if (checkFields(self.$modalForm)) {
- // close modal
- self.modal.hide();
- recurrenceOn(self.$modalForm);
- self.$modalForm = null;
- }
- }
-
- function cancel(event) {
- event.preventDefault();
- // close modal
- self.modal.hide();
- self.$modalForm = null;
- }
-
- function updateOccurrences() {
- var startDate;
- startDate = findStartDate();
-
- // if no field errors, process the request
- if (checkFields(form)) {
- loadOccurrences(
- startDate,
- widgetSaveToRfc5545(
- self.$modalForm,
- textarea["ical"].RDATE,
- textarea["ical"].EXDATE,
- conf,
- false,
- ).result,
- 0,
- false,
- );
- }
- }
-
- function initModalForm() {
- var enddate = self.$modalForm.find("input[name=rirangebyenddatecalendar]");
- if (!enddate.val()) {
- var start_date = findStartDate();
- start_date.setDate(start_date.getDate() + 1);
- enddate.val(start_date.toISOString().substring(0, 10));
- }
-
- self.$modalForm.find("input#addaction").on("click", occurrenceAdd);
-
- // When selecting template, update what fieldsets are visible and the occurrences.
- self.$modalForm.find('select[name="rirtemplate"]').on("change", function () {
- displayFields($(this));
- updateOccurrences();
- });
-
- // Update the selected dates section
- self.$modalForm
- .find(
- `
- input:radio,
- input:checkbox,
- .riweeklyweekday > input,
- input[name=ridailyinterval],
- input[name=riweeklyinterval],
- input[name=rimonthlyinterval],
- input[name=riyearlyinterval],
- input[name=rirangebyoccurrencesvalue],
- input[name=rirangebyenddatecalendar],
- #rimonthlyoptions select,
- #riyearlyoptions select,
- #riyearlyinterval select`,
- )
- .on("change", function () {
- // Update only if the occurances are shown
- if (self.$modalForm.find(".rioccurrencesactions:visible").length !== 0) {
- updateOccurrences();
- }
- });
-
- // initialize occurrence adddate value
- self.$modalForm.find("div.riaddoccurrence input#adddate").val(now_date);
-
- /*
- Save and cancel methods:
- */
- self.$modalForm.find(".ricancelbutton").on("click", cancel);
- self.$modalForm.find(".risavebutton").on("click", save);
- }
-
- /*
- Load the templates
- */
-
- self.display = $(_.template(DisplayTemplate)(conf));
- var form = $(_.template(FormTemplate)(conf));
-
- form.appendTo("body");
- form.hide();
-
- // Make an modal and hide it
- self.modal = new Modal(form, {
- content: "div.riform",
- buttons: ".ributtons > button",
- modalSizeClass: "modal-lg",
- backdropOptions: {
- opacity: "0.5",
- },
- });
-
- if (textarea.innerHTML) {
- var result = widgetLoadFromRfc5545(form, conf, textarea["ical"], false);
- if (result === -1) {
- var label = self.display.find("label[class=ridisplay-label]");
- label.text(conf.localization.noRule);
- } else {
- recurrenceOn(form);
- }
- }
-
- /*
- Do all the GUI stuff:
- */
-
- // When you click "Delete...", the recurrence rules should be cleared.
- self.display.find('button[name="ridelete"]').on("click", function (e) {
- e.preventDefault();
- recurrenceOff();
- });
-
- // Show form modal when you click on the "Edit..." link
- self.display.find('button[name="riedit"]').on("click", function (e) {
- // Load the form to set up the right fields to show, etc.
- e.preventDefault();
- self.modal.show();
- self.$modalForm = $("form", self.modal.$modalContent);
- loadData(self.$modalForm);
- initModalForm();
- });
-};
+// Thin registration module: the implementation (incl. the XML templates and
+// the Modal dependency) is loaded lazily on first match, so it stays out of
+// the eager patterns chunk on pages without a recurrence widget.
export default Base.extend({
name: "recurrence",
trigger: ".pat-recurrence",
parser: "mockup",
- defaults: {
- localization: {
- displayUnactivate: "Does not repeat",
- displayActivate: "Repeats every",
- add_rules: "Add",
- edit_rules: "Edit",
- delete_rules: "Delete",
- add: "Add",
- refresh: "Refresh",
-
- title: "Repeat",
- preview: "Selected dates",
- addDate: "Add date",
-
- recurrenceType: "Repeats:",
-
- dailyInterval1: "Repeat every:",
- dailyInterval2: "days",
-
- weeklyInterval1: "Repeat every:",
- weeklyInterval2: "week(s)",
- weeklyWeekdays: "Repeat on:",
- weeklyWeekdaysHuman: "on:",
-
- monthlyInterval1: "Repeat every:",
- monthlyInterval2: "month(s)",
- monthlyDayOfMonth1: "Day",
- monthlyDayOfMonth1Human: "on day",
- monthlyDayOfMonth2: "of the month",
- monthlyDayOfMonth3: "month(s)",
- monthlyWeekdayOfMonth1: "The",
- monthlyWeekdayOfMonth1Human: "on the",
- monthlyWeekdayOfMonth2: "",
- monthlyWeekdayOfMonth3: "of the month",
- monthlyRepeatOn: "Repeat on:",
-
- yearlyInterval1: "Repeat every:",
- yearlyInterval2: "year(s)",
- yearlyDayOfMonth1: "Every",
- yearlyDayOfMonth1Human: "on",
- yearlyDayOfMonth2: "",
- yearlyDayOfMonth3: "",
- yearlyWeekdayOfMonth1: "The",
- yearlyWeekdayOfMonth1Human: "on the",
- yearlyWeekdayOfMonth2: "",
- yearlyWeekdayOfMonth3: "of",
- yearlyWeekdayOfMonth4: "",
- yearlyRepeatOn: "Repeat on:",
-
- range: "End recurrence:",
- rangeNoEnd: "Never",
- rangeByOccurrences1: "After",
- rangeByOccurrences1Human: "ends after",
- rangeByOccurrences2: "occurrence(s)",
- rangeByEndDate: "On",
- rangeByEndDateHuman: "ends on",
-
- including: ", and also",
- except: ", except for",
-
- cancel: "Cancel",
- save: "Save",
-
- recurrenceStart: "Start of the recurrence",
- additionalDate: "Additional date",
- include: "Include",
- exclude: "Exclude",
- remove: "Remove",
-
- orderIndexes: ["first", "second", "third", "fourth", "last"],
- months: [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
- ],
- shortMonths: [
- "Jan",
- "Feb",
- "Mar",
- "Apr",
- "May",
- "Jun",
- "Jul",
- "Aug",
- "Sep",
- "Oct",
- "Nov",
- "Dec",
- ],
- weekdays: [
- "Sunday",
- "Monday",
- "Tuesday",
- "Wednesday",
- "Thursday",
- "Friday",
- "Saturday",
- ],
- shortWeekdays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
-
- longDateFormat: "mmmm dd, yyyy",
- shortDateFormat: "mm/dd/yyyy",
-
- unsupportedFeatures:
- "Warning: This event uses recurrence features not " +
- "supported by this widget. Saving the recurrence " +
- "may change the recurrence in unintended ways:",
- noTemplateMatch: "No matching recurrence template",
- multipleDayOfMonth:
- "This widget does not support multiple days in monthly or yearly recurrence",
- bysetpos: "BYSETPOS is not supported",
- noRule: "No RRULE in RRULE data",
- noRepeatEvery: 'Error: The "Repeat every"-field must be between 1 and 1000',
- noEndDate: "Error: End date is not set. Please set a correct value",
- noRepeatOn: 'Error: "Repeat on"-value must be selected',
- pastEndDate: "Error: End date cannot be before start date",
- noEndAfterNOccurrences:
- 'Error: The "After N occurrences"-field must be between 1 and 1000',
- alreadyAdded: "This date was already added",
-
- rtemplate: {
- daily: "Daily",
- mondayfriday: "Monday and Friday",
- weekdays: "Weekday",
- weekly: "Weekly",
- monthly: "Monthly",
- yearly: "Yearly",
- },
-
- error_load_occurrences: "Cannot load the occurrences preview.",
- },
-
- readOnly: false,
- firstDay: 0,
-
- // "REMOTE" FIELD
- startField: null,
- startFieldYear: null,
- startFieldMonth: null,
- startFieldDay: null,
- ajaxURL: null,
- ajaxContentType: "application/json; charset=utf8",
- ributtonExtraClass: "",
-
- // INPUT CONFIGURATION
- allowAdditionalDates: false,
- hasRepeatForeverButton: true,
-
- // JQUERY TEMPLATE NAMES
- template: {
- form: "#jquery-recurrenceinput-form-tmpl",
- display: "#jquery-recurrenceinput-display-tmpl",
- },
-
- // RECURRENCE TEMPLATES
- rtemplate: {
- daily: {
- rrule: "FREQ=DAILY",
- fields: ["ridailyinterval", "rirangeoptions"],
- },
- mondayfriday: {
- rrule: "FREQ=WEEKLY;BYDAY=MO,FR",
- fields: ["rirangeoptions"],
- },
- weekdays: {
- rrule: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
- fields: ["rirangeoptions"],
- },
- weekly: {
- rrule: "FREQ=WEEKLY",
- fields: ["riweeklyinterval", "riweeklyweekdays", "rirangeoptions"],
- },
- monthly: {
- rrule: "FREQ=MONTHLY",
- fields: ["rimonthlyinterval", "rimonthlyoptions", "rirangeoptions"],
- },
- yearly: {
- rrule: "FREQ=YEARLY",
- fields: ["riyearlyinterval", "riyearlyoptions", "rirangeoptions"],
- },
- },
- },
-
- load_icons: async function () {
- return {
- reload: await utils.resolveIcon("arrow-clockwise"),
- remove: await utils.resolveIcon("trash"),
- exclude: await utils.resolveIcon("calendar-x"),
- include: await utils.resolveIcon("plus-circle"),
- };
- },
init: async function () {
- // tmpl BEFORE recurrenceinput
- import("./recurrence.scss");
-
- this.el.classList.add("recurrence-widget");
- var icons = await this.load_icons();
-
- var config = {
- ...this.options,
- ...this.options.configuration,
- icons: icons,
- };
-
- // our recurrenceinput widget instance
- var recurrenceinput = new RecurrenceInput(config, this.el);
- // hide textarea and place display widget after textarea
- this.$el.after(recurrenceinput.display);
-
- // hide the textarea
- this.el.style.display = "none";
+ const impl = (await import("./recurrence--implementation")).default;
+ // The options were parsed against empty defaults — the defaults live
+ // with the implementation. Merge them in now, keeping parsed values.
+ this.options = $.extend(true, {}, impl.defaults, this.options);
+ // Graft the implementation onto this instance and run its init.
+ $.extend(this, impl);
+ return impl.init.apply(this, arguments);
},
});
diff --git a/src/pat/relateditems/relateditems.js b/src/pat/relateditems/relateditems.js
index 203664aa80..b26f594bad 100644
--- a/src/pat/relateditems/relateditems.js
+++ b/src/pat/relateditems/relateditems.js
@@ -4,7 +4,6 @@ import Base from "@patternslib/patternslib/src/core/base";
import _t from "../../core/i18n-wrapper";
import utils from "../../core/utils";
import registry from "@patternslib/patternslib/src/core/registry";
-import Select2 from "../select2/select2";
const KEY = {
LEFT: 37,
@@ -402,7 +401,7 @@ export default Base.extend({
},
async initUploadView(disabled) {
- let Upload = await import("../upload/upload");
+ let Upload = await import("../upload/upload--implementation");
Upload = Upload.default;
const upload_button = this.$toolbar[0].querySelector(".upload button");
@@ -501,6 +500,9 @@ export default Base.extend({
},
async init() {
+ // select2 methods are borrowed onto this instance; load them lazily so
+ // the select2 body stays out of the eager patterns chunk.
+ this._select2 = (await import("../select2/select2--implementation")).default;
(await import("bootstrap")).Dropdown;
import("./relateditems.scss");
@@ -525,8 +527,8 @@ export default Base.extend({
this.$el.wrap('');
this.$container = this.$el.parents(".pat-relateditems-container");
- Select2.prototype.initializeValues.call(this);
- Select2.prototype.initializeTags.call(this);
+ this._select2.initializeValues.call(this);
+ this._select2.initializeTags.call(this);
this.options.formatSelection = (item) => {
item = $.extend(
@@ -674,8 +676,8 @@ export default Base.extend({
this.options.id = (item) => item.UID;
- await Select2.prototype.initializeSelect2.call(this);
- await Select2.prototype.initializeOrdering.call(this);
+ await this._select2.initializeSelect2.call(this);
+ await this._select2.initializeOrdering.call(this);
this.$toolbar = $('');
this.$container.prepend(this.$toolbar);
@@ -697,7 +699,7 @@ export default Base.extend({
});
$(document).on("keyup", this.$el, (event) => {
- const isOpen = Select2.prototype.opened.call(this);
+ const isOpen = this._select2.opened.call(this);
if (!isOpen) {
return;
}
diff --git a/src/pat/select2/select2--implementation.js b/src/pat/select2/select2--implementation.js
new file mode 100644
index 0000000000..a90e0c83a6
--- /dev/null
+++ b/src/pat/select2/select2--implementation.js
@@ -0,0 +1,305 @@
+import $ from "jquery";
+import events from "@patternslib/patternslib/src/core/events";
+import I18n from "../../core/i18n";
+import utils from "../../core/utils";
+
+// Pattern config object — grafted onto the thin registered pattern in
+// select2.js and run there. The select2 library, its CSS and the
+// select2_locale_* context all load from here, keeping them (and this
+// wrapper) out of the eager patterns chunk. pat-relateditems borrows
+// these methods via a lazy import of this module.
+export default {
+ defaults: {
+ separator: ",",
+ ajaxTimeout: 300,
+ },
+
+ initializeValues() {
+ // Init Selection ---------------------------------------------
+ if (this.options.initialValues) {
+ this.options.id = (term) => {
+ return term.id;
+ };
+ this.options.initSelection = ($el, callback) => {
+ const data = [];
+ const value = $el.val();
+ let seldefaults = this.options.initialValues;
+
+ // Create the initSelection value that contains the default selection,
+ // but in a javascript object
+ if (
+ typeof this.options.initialValues === "string" &&
+ this.options.initialValues !== ""
+ ) {
+ // if default selection value starts with a '{', then treat the value as
+ // a JSON object that needs to be parsed
+ if (this.options.initialValues[0] === "{") {
+ seldefaults = JSON.parse(this.options.initialValues);
+ }
+ // otherwise, treat the value as a list, separated by the defaults.separator value of
+ // strings in the format "id:text", and convert it to an object
+ else {
+ seldefaults = {};
+ const initial_values = $(
+ this.options.initialValues.split(this.options.separator)
+ );
+ for (const it of initial_values) {
+ const selection = it.split(":");
+ const id = selection[0].trim();
+ const text = selection[1].trim();
+ seldefaults[id] = text;
+ }
+ }
+ }
+
+ const items = $(value.split(this.options.separator));
+ for (const it of items) {
+ let text = it;
+ if (seldefaults[it]) {
+ text = seldefaults[it];
+ }
+ data.push({
+ id: utils.removeHTML(it),
+ text: utils.removeHTML(text),
+ });
+ }
+ callback(data);
+ };
+ }
+ },
+
+ initializeTags() {
+ if (this.options.tags && typeof this.options.tags === "string") {
+ if (this.options.tags.substr(0, 1) === "[") {
+ this.options.tags = JSON.parse(this.options.tags);
+ } else {
+ this.options.tags = this.options.tags.split(this.options.separator);
+ }
+ }
+
+ if (this.options.tags && !this.options.allowNewItems) {
+ this.options.data = this.options.tags.map((value) => {
+ return { id: value, text: value };
+ });
+ this.options.multiple = true;
+ delete this.options.tags;
+ }
+ },
+
+ async initializeOrdering() {
+ if (!this.options.orderable) {
+ return;
+ }
+ const Sortable = (await import("sortablejs")).default;
+ const _initializeOrdering = () => {
+ const sortable_el = this.$select2[0].querySelector(".select2-choices");
+ new Sortable(sortable_el, {
+ draggable: "li",
+ dragClass: "select2-choice-dragging",
+ chosenClass: "dragging",
+ onStart: () => this.$el.select2("onSortStart"),
+ onEnd: () => this.$el.select2("onSortEnd"),
+ });
+ };
+ this.$el.on("change", _initializeOrdering.bind(this));
+ _initializeOrdering();
+ },
+
+ async initializeSelect2() {
+ import("select2/select2.css");
+ import("./select2.scss");
+ await import("select2");
+ try {
+ // Don't load "en" which is the default where no separate language file exists.
+ if (this.options.language && this.options.language !== "en" && !this.options.language.startsWith("en")) {
+ let lang = this.options.language.split("-");
+ // Fix for country specific languages — only for supported combined locales
+ const supportedCombined = new Set(["pt-BR", "pt-PT", "ug-CN", "zh-CN", "zh-TW"]);
+ if(lang.length>1){
+ const combined =`${lang[0]}-${lang[1].toUpperCase()}`;
+ lang = supportedCombined.has(combined) ? combined : lang[0];
+ }else{
+ lang = lang[0];
+ }
+ await import(`select2/select2_locale_${lang}`);
+ }
+ } catch {
+ console.warn("Language file could not be loaded", this.options.language);
+ }
+
+ this.options.formatResultCssClass = (ob) => {
+ if (ob.id) {
+ return (
+ "select2-option-" +
+ ob.id.toLowerCase().replace(/[ \:\)\(\[\]\{\}\_\+\=\&\*\%\#]/g, "-")
+ );
+ }
+ };
+
+ function callback(action, e) {
+ if (action) {
+ if (this.options.debug) {
+ console.debug("callback", action, e);
+ }
+ if (typeof action === "string") {
+ action = window[action];
+ }
+ return action(e);
+ } else {
+ return action;
+ }
+ }
+
+ this.$el.select2(this.options);
+
+ // Select2 v3 signals changes via a jQuery `change` event, which isn't
+ // caught by native JavaScript event listeners.
+ // Let's re-trigger as native `input`- and `change`-events, so that
+ // those can pick it up. A native `