summaryrefslogtreecommitdiff
path: root/addons/website_blog/static/src/js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website_blog/static/src/js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website_blog/static/src/js')
-rw-r--r--addons/website_blog/static/src/js/contentshare.js110
-rw-r--r--addons/website_blog/static/src/js/tours/website_blog.js79
-rw-r--r--addons/website_blog/static/src/js/website_blog.editor.js380
-rw-r--r--addons/website_blog/static/src/js/website_blog.js102
4 files changed, 671 insertions, 0 deletions
diff --git a/addons/website_blog/static/src/js/contentshare.js b/addons/website_blog/static/src/js/contentshare.js
new file mode 100644
index 00000000..2e6c9cd4
--- /dev/null
+++ b/addons/website_blog/static/src/js/contentshare.js
@@ -0,0 +1,110 @@
+odoo.define('website_blog.contentshare', function (require) {
+'use strict';
+
+const dom = require('web.dom');
+
+$.fn.share = function (options) {
+ var option = $.extend($.fn.share.defaults, options);
+ var selected_text = "";
+ $.extend($.fn.share, {
+ init: function (shareable) {
+ var self = this;
+ $.fn.share.defaults.shareable = shareable;
+ $.fn.share.defaults.shareable.on('mouseup', function () {
+ if ($(this).parents('body.editor_enable').length === 0) {
+ self.popOver();
+ }
+ });
+ $.fn.share.defaults.shareable.on('mousedown', function () {
+ self.destroy();
+ });
+ },
+ getContent: function () {
+ var $popover_content = $('<div class="h4 m-0"/>');
+ if ($('.o_wblog_title, .o_wblog_post_content_field').hasClass('js_comment')) {
+ selected_text = this.getSelection('string');
+ var $btn_c = $('<a class="o_share_comment btn btn-link px-2" href="#"/>').append($('<i class="fa fa-lg fa-comment"/>'));
+ $popover_content.append($btn_c);
+ }
+ if ($('.o_wblog_title, .o_wblog_post_content_field').hasClass('js_tweet')) {
+ var tweet = '"%s" - %s';
+ var baseLength = tweet.replace(/%s/g, '').length;
+ // Shorten the selected text to match the tweet max length
+ // Note: all (non-localhost) urls in a tweet have 23 characters https://support.twitter.com/articles/78124
+ var selectedText = this.getSelection('string').substring(0, option.maxLength - baseLength - 23);
+
+ var text = window.btoa(encodeURIComponent(_.str.sprintf(tweet, selectedText, window.location.href)));
+ $popover_content.append(_.str.sprintf(
+ "<a onclick=\"window.open('%s' + atob('%s'), '_%s','location=yes,height=570,width=520,scrollbars=yes,status=yes')\"><i class=\"ml4 mr4 fa fa-twitter fa-lg\"/></a>",
+ option.shareLink, text, option.target));
+ }
+ return $popover_content;
+ },
+ commentEdition: function () {
+ $(".o_portal_chatter_composer_form textarea").val('"' + selected_text + '" ').focus();
+ const commentsEl = $('#o_wblog_post_comments')[0];
+ if (commentsEl) {
+ dom.scrollTo(commentsEl).then(() => {
+ window.location.hash = 'blog_post_comment_quote';
+ });
+ }
+ },
+ getSelection: function (share) {
+ if (window.getSelection) {
+ var selection = window.getSelection();
+ if (!selection || selection.rangeCount === 0) {
+ return "";
+ }
+ if (share === 'string') {
+ return String(selection.getRangeAt(0)).replace(/\s{2,}/g, ' ');
+ } else {
+ return selection.getRangeAt(0);
+ }
+ } else if (document.selection) {
+ if (share === 'string') {
+ return document.selection.createRange().text.replace(/\s{2,}/g, ' ');
+ } else {
+ return document.selection.createRange();
+ }
+ }
+ },
+ popOver: function () {
+ this.destroy();
+ if (this.getSelection('string').length < option.minLength) {
+ return;
+ }
+ var data = this.getContent();
+ var range = this.getSelection();
+
+ var newNode = document.createElement("span");
+ range.insertNode(newNode);
+ newNode.className = option.className;
+ var $pop = $(newNode);
+ $pop.popover({
+ trigger: 'manual',
+ placement: option.placement,
+ html: true,
+ content: function () {
+ return data;
+ }
+ }).popover('show');
+ $('.o_share_comment').on('click', this.commentEdition);
+ },
+ destroy: function () {
+ var $span = $('span.' + option.className);
+ $span.popover('hide');
+ $span.remove();
+ }
+ });
+ $.fn.share.init(this);
+};
+
+$.fn.share.defaults = {
+ shareLink: "http://twitter.com/intent/tweet?text=",
+ minLength: 5,
+ maxLength: 140,
+ target: "blank",
+ className: "share",
+ placement: "top",
+};
+});
diff --git a/addons/website_blog/static/src/js/tours/website_blog.js b/addons/website_blog/static/src/js/tours/website_blog.js
new file mode 100644
index 00000000..23b59149
--- /dev/null
+++ b/addons/website_blog/static/src/js/tours/website_blog.js
@@ -0,0 +1,79 @@
+odoo.define("website_blog.tour", function (require) {
+ "use strict";
+
+ var core = require("web.core");
+ var tour = require("web_tour.tour");
+
+ var _t = core._t;
+
+ tour.register("blog", {
+ url: "/",
+ }, [{
+ trigger: '#new-content-menu > a',
+ content: _t("Click here to add new content to your website."),
+ position: 'bottom',
+
+ }, {
+ trigger: "a[data-action=new_blog_post]",
+ content: _t("Select this menu item to create a new blog post."),
+ position: "bottom",
+ }, {
+ trigger: "button.btn-continue",
+ extra_trigger: "form[id=\"editor_new_blog\"]",
+ content: _t("Select the blog you want to add the post to."),
+ }, {
+ trigger: "div[data-oe-expression=\"blog_post.name\"]",
+ extra_trigger: "#oe_snippets.o_loaded",
+ content: _t("Write a title, the subtitle is optional."),
+ position: "top",
+ run: "text",
+ }, {
+ trigger: "we-button[data-background]:nth(1)",
+ extra_trigger: "#wrap div[data-oe-expression=\"blog_post.name\"]:not(:containsExact(\"\"))",
+ content: _t("Set a blog post <b>cover</b>."),
+ position: "right",
+ }, {
+ trigger: ".o_select_media_dialog .o_we_search",
+ content: _t("Search for an image. (eg: type \"business\")"),
+ position: "top",
+ }, {
+ trigger: ".o_select_media_dialog .o_existing_attachment_cell:first img",
+ extra_trigger: '.modal:has(.o_existing_attachment_cell:first)',
+ content: _t("Choose an image from the library."),
+ position: "top",
+ }, {
+ trigger: "#o_wblog_post_content",
+ content: _t("<b>Write your story here.</b> Use the top toolbar to style your text: add an image or table, set bold or italic, etc. Drag and drop building blocks for more graphical blogs."),
+ position: "top",
+ run: function (actions) {
+ actions.auto();
+ actions.text("Blog content", this.$anchor.find("p"));
+ },
+ }, {
+ trigger: "button[data-action=save]",
+ extra_trigger: "#o_wblog_post_content .o_wblog_post_content_field p:first:not(:containsExact(" + _t("Start writing here...") + "))",
+ content: _t("<b>Click on Save</b> to record your changes."),
+ position: "bottom",
+ }, {
+ trigger: "a[data-action=show-mobile-preview]",
+ extra_trigger: "body:not(.editor_enable)",
+ content: _t("Use this icon to preview your blog post on <b>mobile devices</b>."),
+ position: "bottom",
+ }, {
+ trigger: "button[data-dismiss=modal]",
+ extra_trigger: '.modal:has(#mobile-viewport)',
+ content: _t("Once you have reviewed the content on mobile, close the preview."),
+ position: "right",
+ }, {
+ trigger: ".js_publish_management .js_publish_btn",
+ extra_trigger: "body:not(.editor_enable)",
+ position: "bottom",
+ content: _t("<b>Publish your blog post</b> to make it visible to your visitors."),
+ }, {
+ trigger: "#customize-menu > a",
+ extra_trigger: ".js_publish_management .js_publish_btn .css_unpublish:visible",
+ content: _t("<b>That's it, your blog post is published!</b> Discover more features through the <i>Customize</i> menu."),
+ position: "bottom",
+ width: 500,
+ }]);
+});
diff --git a/addons/website_blog/static/src/js/website_blog.editor.js b/addons/website_blog/static/src/js/website_blog.editor.js
new file mode 100644
index 00000000..95daa0fb
--- /dev/null
+++ b/addons/website_blog/static/src/js/website_blog.editor.js
@@ -0,0 +1,380 @@
+odoo.define('website_blog.new_blog_post', function (require) {
+'use strict';
+
+var core = require('web.core');
+var wUtils = require('website.utils');
+var WebsiteNewMenu = require('website.newMenu');
+
+var _t = core._t;
+
+WebsiteNewMenu.include({
+ actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
+ new_blog_post: '_createNewBlogPost',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Actions
+ //--------------------------------------------------------------------------
+
+ /**
+ * Asks the user information about a new blog post to create, then creates
+ * it and redirects the user to this new post.
+ *
+ * @private
+ * @returns {Promise} Unresolved if there is a redirection
+ */
+ _createNewBlogPost: function () {
+ return this._rpc({
+ model: 'blog.blog',
+ method: 'search_read',
+ args: [wUtils.websiteDomain(this), ['name']],
+ }).then(function (blogs) {
+ if (blogs.length === 1) {
+ document.location = '/blog/' + blogs[0]['id'] + '/post/new';
+ return new Promise(function () {});
+ } else if (blogs.length > 1) {
+ return wUtils.prompt({
+ id: 'editor_new_blog',
+ window_title: _t("New Blog Post"),
+ select: _t("Select Blog"),
+ init: function (field) {
+ return _.map(blogs, function (blog) {
+ return [blog['id'], blog['name']];
+ });
+ },
+ }).then(function (result) {
+ var blog_id = result.val;
+ if (!blog_id) {
+ return;
+ }
+ document.location = '/blog/' + blog_id + '/post/new';
+ return new Promise(function () {});
+ });
+ }
+ });
+ },
+});
+});
+
+//==============================================================================
+
+odoo.define('website_blog.editor', function (require) {
+'use strict';
+
+require('web.dom_ready');
+const {qweb, _t} = require('web.core');
+const options = require('web_editor.snippets.options');
+var WysiwygMultizone = require('web_editor.wysiwyg.multizone');
+
+if (!$('.website_blog').length) {
+ return Promise.reject("DOM doesn't contain '.website_blog'");
+}
+
+const NEW_TAG_PREFIX = 'new-blog-tag-';
+
+WysiwygMultizone.include({
+ custom_events: Object.assign({}, WysiwygMultizone.prototype.custom_events, {
+ 'set_blog_post_updated_tags': '_onSetBlogPostUpdatedTags',
+ }),
+
+ /**
+ * @override
+ */
+ init() {
+ this._super(...arguments);
+ this.blogTagsPerBlogPost = {};
+ },
+ /**
+ * @override
+ */
+ async start() {
+ await this._super(...arguments);
+ $('.js_tweet, .js_comment').off('mouseup').trigger('mousedown');
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async save() {
+ const ret = await this._super(...arguments);
+ await this._saveBlogTags(); // Note: important to be called after save otherwise cleanForSave is not called before
+ return ret;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Saves the blog tags in the database.
+ *
+ * @private
+ */
+ async _saveBlogTags() {
+ for (const [key, tags] of Object.entries(this.blogTagsPerBlogPost)) {
+ const proms = tags.filter(tag => typeof tag.id === 'string').map(tag => {
+ return this._rpc({
+ model: 'blog.tag',
+ method: 'create',
+ args: [{
+ 'name': tag.name,
+ }],
+ });
+ });
+ const createdIDs = await Promise.all(proms);
+
+ await this._rpc({
+ model: 'blog.post',
+ method: 'write',
+ args: [parseInt(key), {
+ 'tag_ids': [[6, 0, tags.filter(tag => typeof tag.id === 'number').map(tag => tag.id).concat(createdIDs)]],
+ }],
+ });
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onSetBlogPostUpdatedTags: function (ev) {
+ this.blogTagsPerBlogPost[ev.data.blogPostID] = ev.data.tags;
+ },
+});
+
+options.registry.many2one.include({
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _selectRecord: function ($opt) {
+ var self = this;
+ this._super.apply(this, arguments);
+ if (this.$target.data('oe-field') === 'author_id') {
+ var $nodes = $('[data-oe-model="blog.post"][data-oe-id="'+this.$target.data('oe-id')+'"][data-oe-field="author_avatar"]');
+ $nodes.each(function () {
+ var $img = $(this).find('img');
+ var css = window.getComputedStyle($img[0]);
+ $img.css({ width: css.width, height: css.height });
+ $img.attr('src', '/web/image/res.partner/'+self.ID+'/image_1024');
+ });
+ setTimeout(function () { $nodes.removeClass('o_dirty'); },0);
+ }
+ }
+});
+
+options.registry.CoverProperties.include({
+ /**
+ * @override
+ */
+ updateUI: async function () {
+ await this._super(...arguments);
+ var isRegularCover = this.$target.is('.o_wblog_post_page_cover_regular');
+ var $coverFull = this.$el.find('[data-select-class*="o_full_screen_height"]');
+ var $coverMid = this.$el.find('[data-select-class*="o_half_screen_height"]');
+ var $coverAuto = this.$el.find('[data-select-class*="cover_auto"]');
+ this._coverFullOriginalLabel = this._coverFullOriginalLabel || $coverFull.text();
+ this._coverMidOriginalLabel = this._coverMidOriginalLabel || $coverMid.text();
+ this._coverAutoOriginalLabel = this._coverAutoOriginalLabel || $coverAuto.text();
+ $coverFull.children('div').text(isRegularCover ? _t("Large") : this._coverFullOriginalLabel);
+ $coverMid.children('div').text(isRegularCover ? _t("Medium") : this._coverMidOriginalLabel);
+ $coverAuto.children('div').text(isRegularCover ? _t("Tiny") : this._coverAutoOriginalLabel);
+ },
+});
+
+options.registry.BlogPostTagSelection = options.Class.extend({
+ xmlDependencies: (options.Class.prototype.xmlDependencies || [])
+ .concat(['/website_blog/static/src/xml/website_blog_tag.xml']),
+
+ /**
+ * @override
+ */
+ async willStart() {
+ const _super = this._super.bind(this);
+
+ this.blogPostID = parseInt(this.$target[0].dataset.blogId);
+ this.isEditingTags = false;
+ const tags = await this._rpc({
+ model: 'blog.tag',
+ method: 'search_read',
+ args: [[], ['id', 'name', 'post_ids']],
+ });
+ this.allTagsByID = {};
+ this.tagIDs = [];
+ for (const tag of tags) {
+ this.allTagsByID[tag.id] = tag;
+ if (tag['post_ids'].includes(this.blogPostID)) {
+ this.tagIDs.push(tag.id);
+ }
+ }
+
+ return _super(...arguments);
+ },
+ /**
+ * @override
+ */
+ cleanForSave() {
+ if (this.isEditingTags) {
+ this._notifyUpdatedTags();
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * @see this.selectClass for params
+ */
+ editTagList(previewMode, widgetValue, params) {
+ this.isEditingTags = true;
+ this.rerender = true;
+ },
+ /**
+ * Send changes that will be saved in the database.
+ *
+ * @see this.selectClass for params
+ */
+ saveTagList(previewMode, widgetValue, params) {
+ this.isEditingTags = false;
+ this.rerender = true;
+ this._notifyUpdatedTags();
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setNewTagName(previewMode, widgetValue, params) {
+ this.newTagName = widgetValue;
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ confirmNew(previewMode, widgetValue, params) {
+ if (!this.newTagName) {
+ return;
+ }
+ const existing = Object.values(this.allTagsByID).some(tag => tag.name.toLowerCase() === this.newTagName.toLowerCase());
+ if (existing) {
+ return this.displayNotification({
+ type: 'warning',
+ message: _t("This tag already exists"),
+ });
+ }
+ const newTagID = _.uniqueId(NEW_TAG_PREFIX);
+ this.allTagsByID[newTagID] = {
+ 'id': newTagID,
+ 'name': this.newTagName,
+ };
+ this.tagIDs.push(newTagID);
+ this.newTagName = '';
+ this.rerender = true;
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ addTag(previewMode, widgetValue, params) {
+ const tagID = parseInt(widgetValue);
+ this.tagIDs.push(tagID);
+ this.rerender = true;
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ removeTag(previewMode, widgetValue, params) {
+ this.tagIDs = this.tagIDs.filter(tagID => (`${tagID}` !== widgetValue));
+ if (widgetValue.startsWith(NEW_TAG_PREFIX)) {
+ delete this.allTagsByID[widgetValue];
+ }
+ this.rerender = true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async updateUI() {
+ if (this.rerender) {
+ this.rerender = false;
+ await this._rerenderXML();
+ return;
+ }
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async _computeWidgetVisibility(widgetName, params) {
+ if (['blog_existing_tag_opt', 'new_tag_input_opt', 'new_tag_button_opt', 'save_tags_opt'].includes(widgetName)) {
+ return this.isEditingTags;
+ }
+ if (widgetName === 'edit_tags_opt') {
+ return !this.isEditingTags;
+ }
+ if (params.optionsPossibleValues['removeTag']) {
+ return this.isEditingTags;
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ async _computeWidgetState(methodName, params) {
+ if (methodName === 'addTag') {
+ // The related widget allows to select a value but then resets its state to a non-selected value
+ return '';
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @private
+ */
+ _notifyUpdatedTags() {
+ this.trigger_up('set_blog_post_updated_tags', {
+ blogPostID: this.blogPostID,
+ tags: this.tagIDs.map(tagID => this.allTagsByID[tagID]),
+ });
+ },
+ /**
+ * @override
+ */
+ async _renderCustomXML(uiFragment) {
+ const $tagList = $(uiFragment.querySelector('.o_wblog_tag_list'));
+ for (const tagID of this.tagIDs) {
+ const tag = this.allTagsByID[tagID];
+ $tagList.append(qweb.render('website_blog.TagListItem', {
+ tag: tag,
+ }));
+ }
+ const $select = $(uiFragment.querySelector('we-select[data-name="blog_existing_tag_opt"]'));
+ for (const [key, tag] of Object.entries(this.allTagsByID)) {
+ if (this.tagIDs.includes(parseInt(key)) || this.tagIDs.includes(key)) {
+ // saved tag keys are numbers, new tag keys are strings
+ continue;
+ }
+ $select.prepend(qweb.render('website_blog.TagSelectItem', {
+ tag: tag,
+ }));
+ }
+ },
+});
+});
diff --git a/addons/website_blog/static/src/js/website_blog.js b/addons/website_blog/static/src/js/website_blog.js
new file mode 100644
index 00000000..7131f275
--- /dev/null
+++ b/addons/website_blog/static/src/js/website_blog.js
@@ -0,0 +1,102 @@
+odoo.define('website_blog.website_blog', function (require) {
+'use strict';
+var core = require('web.core');
+
+const dom = require('web.dom');
+const publicWidget = require('web.public.widget');
+
+publicWidget.registry.websiteBlog = publicWidget.Widget.extend({
+ selector: '.website_blog',
+ events: {
+ 'click #o_wblog_next_container': '_onNextBlogClick',
+ 'click #o_wblog_post_content_jump': '_onContentAnchorClick',
+ 'click .o_twitter, .o_facebook, .o_linkedin, .o_google, .o_twitter_complete, .o_facebook_complete, .o_linkedin_complete, .o_google_complete': '_onShareArticle',
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ $('.js_tweet, .js_comment').share({});
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onNextBlogClick: function (ev) {
+ ev.preventDefault();
+ var self = this;
+ var $el = $(ev.currentTarget);
+ var nexInfo = $el.find('#o_wblog_next_post_info').data();
+ $el.find('.o_record_cover_container').addClass(nexInfo.size + ' ' + nexInfo.text).end()
+ .find('.o_wblog_toggle').toggleClass('d-none');
+ // Appending a placeholder so that the cover can scroll to the top of the
+ // screen, regardless of its height.
+ const placeholder = document.createElement('div');
+ placeholder.style.minHeight = '100vh';
+ this.$('#o_wblog_next_container').append(placeholder);
+
+ // Use _.defer to calculate the 'offset()'' only after that size classes
+ // have been applyed and that $el has been resized.
+ _.defer(function () {
+ self._forumScrollAction($el, 300, function () {
+ window.location.href = nexInfo.url;
+ });
+ });
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onContentAnchorClick: function (ev) {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ var $el = $(ev.currentTarget.hash);
+
+ this._forumScrollAction($el, 500, function () {
+ window.location.hash = 'blog_content';
+ });
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onShareArticle: function (ev) {
+ ev.preventDefault();
+ var url = '';
+ var $element = $(ev.currentTarget);
+ var blogPostTitle = encodeURIComponent($('#o_wblog_post_name').html() || '');
+ var articleURL = encodeURIComponent(window.location.href);
+ if ($element.hasClass('o_twitter')) {
+ var twitterText = core._t("Amazing blog article: %s! Check it live: %s");
+ var tweetText = _.string.sprintf(twitterText, blogPostTitle, articleURL);
+ url = 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=' + tweetText;
+ } else if ($element.hasClass('o_facebook')) {
+ url = 'https://www.facebook.com/sharer/sharer.php?u=' + articleURL;
+ } else if ($element.hasClass('o_linkedin')) {
+ url = 'https://www.linkedin.com/sharing/share-offsite/?url=' + articleURL;
+ }
+ window.open(url, '', 'menubar=no, width=500, height=400');
+ },
+
+ //--------------------------------------------------------------------------
+ // Utils
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {JQuery} $el - the element we are scrolling to
+ * @param {Integer} duration - scroll animation duration
+ * @param {Function} callback - to be executed after the scroll is performed
+ */
+ _forumScrollAction: function ($el, duration, callback) {
+ dom.scrollTo($el[0], {duration: duration}).then(() => callback());
+ },
+});
+});