From 963842381e2449ee59e5cffd1bae8656f5477e7a Mon Sep 17 00:00:00 2001 From: Pierre Paridans Date: Sun, 17 Sep 2023 09:46:01 +0200 Subject: [PATCH] [IMP] web,*: replace ClipboardJS with Web API and homebrew polyfill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *: auth_totp_portal, mrp_subcontracting, project, survey, web_editor, website, website_links, website_sale_loyalty, website_slides, test_lint, spreadsheet Implements missing `clipboard.write()` and `ClipboardItem` ; targets only Firefox (which partially implements Clipboard API, features hidden behind a preference flag). Supported browsers: Chromium 76+, Safari 13.1+, Firefox 63+, Safari iOS 13.4+. This allows to replace ClipboardJS by the native Clipboard Web API. Reference: https://caniuse.com/?search=clipboard https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API https://w3c.github.io/clipboard-apis/ task-3439226 closes odoo/odoo#134881 Related: odoo/enterprise#47154 Signed-off-by: Adrien Dieudonné (adr) --- .../static/src/js/totp_frontend.js | 20 +- addons/mrp_subcontracting/__manifest__.py | 1 - addons/project/__manifest__.py | 1 - addons/spreadsheet/__manifest__.py | 1 + .../static/src/js/survey_session_manage.js | 25 +- addons/web/__manifest__.py | 4 +- addons/web/static/lib/clipboard/clipboard.js | 939 ------------------ addons/web/static/src/polyfills/clipboard.js | 100 ++ .../fields/copy_clipboard/copy_button.js | 3 - .../fields/copy_clipboard_field_tests.js | 33 - addons/web/tooling/_eslintrc.json | 1 - addons/web_editor/__manifest__.py | 1 - .../js/wysiwyg/widgets/link_popover_widget.js | 45 +- .../static/src/js/editor/snippets.options.js | 8 +- .../tours/snippet_editor_panel_options.js | 12 +- .../tours/snippet_popup_display_on_click.js | 12 +- .../static/src/js/website_links.js | 22 +- .../static/src/js/website_links_charts.js | 10 +- .../static/src/xml/recent_link.xml | 2 +- .../static/tests/tours/website_links.js | 5 + .../static/src/js/website_sale_gift_card.js | 14 +- addons/website_slides/__manifest__.py | 1 - .../src/js/slides_course_fullscreen_player.js | 21 +- .../static/src/js/slides_share.js | 50 +- debian/copyright | 4 - odoo/addons/test_lint/tests/eslintrc | 1 - 26 files changed, 193 insertions(+), 1143 deletions(-) delete mode 100644 addons/web/static/lib/clipboard/clipboard.js create mode 100644 addons/web/static/src/polyfills/clipboard.js diff --git a/addons/auth_totp_portal/static/src/js/totp_frontend.js b/addons/auth_totp_portal/static/src/js/totp_frontend.js index fe0ce43118c..443a5872ab5 100644 --- a/addons/auth_totp_portal/static/src/js/totp_frontend.js +++ b/addons/auth_totp_portal/static/src/js/totp_frontend.js @@ -6,6 +6,7 @@ import { InputConfirmationDialog } from "@portal/js/components/input_confirmatio import { handleCheckIdentity } from "@portal/js/portal_security"; import publicWidget from "@web/legacy/js/public/public_widget"; import { session } from "@web/session"; +import { browser } from "@web/core/browser/browser"; /** * Replaces specific elements by normal HTML, strip out the rest entirely @@ -47,23 +48,12 @@ function fromField(f, record) { const copyButton = document.createElement('button'); copyButton.setAttribute('class', 'btn btn-sm btn-primary o_clipboard_button o_btn_char_copy py-0 px-2'); - copyButton.onclick = function(event) { + copyButton.onclick = async function(event) { event.preventDefault(); $(copyButton).tooltip({title: _t("Copied!"), trigger: "manual", placement: "bottom"}); - var clipboard = new ClipboardJS('.o_clipboard_button', { - target: function () { - return $(secretSpan)[0]; - }, - container: this.el - }); - clipboard.on('success', function () { - clipboard.destroy(); - $(copyButton).tooltip('show'); - setTimeout(() => $(copyButton).tooltip("hide"), 800); - }); - clipboard.on('error', function (e) { - clipboard.destroy(); - }); + await browser.navigator.clipboard.writeText($(secretSpan)[0].innerText); + $(copyButton).tooltip('show'); + setTimeout(() => $(copyButton).tooltip("hide"), 800); }; copyButton.appendChild(copySpanIcon); diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py index 3740563ac74..745b5c39149 100644 --- a/addons/mrp_subcontracting/__manifest__.py +++ b/addons/mrp_subcontracting/__manifest__.py @@ -84,7 +84,6 @@ 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', 'web/static/lib/select2/select2.js', - 'web/static/lib/clipboard/clipboard.js', 'web/static/src/legacy/js/libs/autocomplete.js', 'web/static/src/legacy/js/libs/bootstrap.js', 'web/static/src/legacy/js/libs/jquery.js', diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index 907aae927b5..2420bd4926b 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -134,7 +134,6 @@ 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', 'web/static/lib/select2/select2.js', - 'web/static/lib/clipboard/clipboard.js', 'web/static/src/legacy/js/libs/autocomplete.js', 'web/static/src/legacy/js/libs/bootstrap.js', 'web/static/src/legacy/js/libs/jquery.js', diff --git a/addons/spreadsheet/__manifest__.py b/addons/spreadsheet/__manifest__.py index 0bcc6b112b5..4ae3c892191 100644 --- a/addons/spreadsheet/__manifest__.py +++ b/addons/spreadsheet/__manifest__.py @@ -18,6 +18,7 @@ 'web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js', ], 'spreadsheet.o_spreadsheet': [ + 'web/static/src/polyfills/clipboard.js', 'spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js', 'spreadsheet/static/src/**/*.js', # Load all o_spreadsheet templates first to allow to inherit them diff --git a/addons/survey/static/src/js/survey_session_manage.js b/addons/survey/static/src/js/survey_session_manage.js index 5c942936afa..d7468a41105 100644 --- a/addons/survey/static/src/js/survey_session_manage.js +++ b/addons/survey/static/src/js/survey_session_manage.js @@ -6,6 +6,7 @@ import SurveySessionChart from "@survey/js/survey_session_chart"; import SurveySessionTextAnswers from "@survey/js/survey_session_text_answers"; import SurveySessionLeaderBoard from "@survey/js/survey_session_leaderboard"; import { _t } from "@web/core/l10n/translation"; +import { browser } from "@web/core/browser/browser"; const nextPageTooltips = { closingWords: _t('End of Survey'), @@ -93,12 +94,11 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend(SurveyPre /** * Copies the survey URL link to the clipboard. - * We use 'ClipboardJS' to avoid having to print the URL in a standard text input + * We avoid having to print the URL in a standard text input. * * @param {MouseEvent} ev */ - _onCopySessionLink: function (ev) { - var self = this; + _onCopySessionLink: async function (ev) { ev.preventDefault(); var $clipboardBtn = this.$('.o_survey_session_copy'); @@ -113,22 +113,9 @@ publicWidget.registry.SurveySessionManage = publicWidget.Widget.extend(SurveyPre } }); - var clipboard = new ClipboardJS('.o_survey_session_copy', { - text: function () { - return self.$('.o_survey_session_copy_url').val(); - }, - container: this.el - }); - - clipboard.on('success', function () { - clipboard.destroy(); - $clipboardBtn.popover('show'); - setTimeout(() => $clipboardBtn.popover('dispose'), 800); - }); - - clipboard.on('error', function (e) { - clipboard.destroy(); - }); + await browser.navigator.clipboard.writeText(this.$('.o_survey_session_copy_url').val()); + $clipboardBtn.popover('show'); + setTimeout(() => $clipboardBtn.popover('dispose'), 800); }, /** diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 31991dbaeaf..77583262815 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -62,6 +62,8 @@ This module provides the core of the Odoo Web Client. 'web/static/src/views/fields/translation_dialog.scss', 'web/static/src/legacy/scss/ui.scss', + 'web/static/src/polyfills/clipboard.js', + 'web/static/lib/jquery/jquery.js', 'web/static/lib/jquery.ui/jquery-ui.js', 'web/static/lib/popper/popper.js', @@ -82,7 +84,6 @@ This module provides the core of the Odoo Web Client. 'web/static/lib/bootstrap/js/dist/scrollspy.js', 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', - 'web/static/lib/clipboard/clipboard.js', 'web/static/src/legacy/js/libs/autocomplete.js', 'web/static/src/legacy/js/libs/bootstrap.js', 'web/static/src/legacy/js/libs/jquery.js', @@ -192,7 +193,6 @@ This module provides the core of the Odoo Web Client. 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', 'web/static/lib/select2/select2.js', - 'web/static/lib/clipboard/clipboard.js', 'web/static/src/legacy/js/libs/autocomplete.js', 'web/static/src/legacy/js/libs/bootstrap.js', 'web/static/src/legacy/js/libs/jquery.js', diff --git a/addons/web/static/lib/clipboard/clipboard.js b/addons/web/static/lib/clipboard/clipboard.js deleted file mode 100644 index a61b0248887..00000000000 --- a/addons/web/static/lib/clipboard/clipboard.js +++ /dev/null @@ -1,939 +0,0 @@ -/*! - * clipboard.js v2.0.0 - * https://zenorocha.github.io/clipboard.js - * - * Licensed MIT © Zeno Rocha - */ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(typeof define === 'function' && define.amd) - define([], factory); - else if(typeof exports === 'object') - exports["ClipboardJS"] = factory(); - else - root["ClipboardJS"] = factory(); -})(this, function() { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // identity function for calling harmony imports with the correct context -/******/ __webpack_require__.i = function(value) { return value; }; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { -/******/ configurable: false, -/******/ enumerable: true, -/******/ get: getter -/******/ }); -/******/ } -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 3); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (global, factory) { - if (true) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [module, __webpack_require__(7)], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports !== "undefined") { - factory(module, require('select')); - } else { - var mod = { - exports: {} - }; - factory(mod, global.select); - global.clipboardAction = mod.exports; - } -})(this, function (module, _select) { - 'use strict'; - - var _select2 = _interopRequireDefault(_select); - - function _interopRequireDefault(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - } - - var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { - return typeof obj; - } : function (obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }; - - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - } - - var _createClass = function () { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); - - var ClipboardAction = function () { - /** - * @param {Object} options - */ - function ClipboardAction(options) { - _classCallCheck(this, ClipboardAction); - - this.resolveOptions(options); - this.initSelection(); - } - - /** - * Defines base properties passed from constructor. - * @param {Object} options - */ - - - _createClass(ClipboardAction, [{ - key: 'resolveOptions', - value: function resolveOptions() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - this.action = options.action; - this.container = options.container; - this.emitter = options.emitter; - this.target = options.target; - this.text = options.text; - this.trigger = options.trigger; - - this.selectedText = ''; - } - }, { - key: 'initSelection', - value: function initSelection() { - if (this.text) { - this.selectFake(); - } else if (this.target) { - this.selectTarget(); - } - } - }, { - key: 'selectFake', - value: function selectFake() { - var _this = this; - - var isRTL = document.documentElement.getAttribute('dir') == 'rtl'; - - this.removeFake(); - - this.fakeHandlerCallback = function () { - return _this.removeFake(); - }; - this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true; - - this.fakeElem = document.createElement('textarea'); - // Prevent zooming on iOS - this.fakeElem.style.fontSize = '12pt'; - // Reset box model - this.fakeElem.style.border = '0'; - this.fakeElem.style.padding = '0'; - this.fakeElem.style.margin = '0'; - // Move element out of screen horizontally - this.fakeElem.style.position = 'absolute'; - this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px'; - // Move element to the same position vertically - var yPosition = window.pageYOffset || document.documentElement.scrollTop; - this.fakeElem.style.top = yPosition + 'px'; - - this.fakeElem.setAttribute('readonly', ''); - this.fakeElem.value = this.text; - - this.container.appendChild(this.fakeElem); - - this.selectedText = (0, _select2.default)(this.fakeElem); - this.copyText(); - } - }, { - key: 'removeFake', - value: function removeFake() { - if (this.fakeHandler) { - this.container.removeEventListener('click', this.fakeHandlerCallback); - this.fakeHandler = null; - this.fakeHandlerCallback = null; - } - - if (this.fakeElem) { - this.container.removeChild(this.fakeElem); - this.fakeElem = null; - } - } - }, { - key: 'selectTarget', - value: function selectTarget() { - this.selectedText = (0, _select2.default)(this.target); - this.copyText(); - } - }, { - key: 'copyText', - value: function copyText() { - var succeeded = void 0; - - try { - succeeded = document.execCommand(this.action); - } catch (err) { - succeeded = false; - } - - this.handleResult(succeeded); - } - }, { - key: 'handleResult', - value: function handleResult(succeeded) { - this.emitter.emit(succeeded ? 'success' : 'error', { - action: this.action, - text: this.selectedText, - trigger: this.trigger, - clearSelection: this.clearSelection.bind(this) - }); - } - }, { - key: 'clearSelection', - value: function clearSelection() { - if (this.trigger) { - this.trigger.focus(); - } - - window.getSelection().removeAllRanges(); - } - }, { - key: 'destroy', - value: function destroy() { - this.removeFake(); - } - }, { - key: 'action', - set: function set() { - var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy'; - - this._action = action; - - if (this._action !== 'copy' && this._action !== 'cut') { - throw new Error('Invalid "action" value, use either "copy" or "cut"'); - } - }, - get: function get() { - return this._action; - } - }, { - key: 'target', - set: function set(target) { - if (target !== undefined) { - if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) { - if (this.action === 'copy' && target.hasAttribute('disabled')) { - throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'); - } - - if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) { - throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes'); - } - - this._target = target; - } else { - throw new Error('Invalid "target" value, use a valid Element'); - } - } - }, - get: function get() { - return this._target; - } - }]); - - return ClipboardAction; - }(); - - module.exports = ClipboardAction; -}); - -/***/ }), -/* 1 */ -/***/ (function(module, exports, __webpack_require__) { - -var is = __webpack_require__(6); -var delegate = __webpack_require__(5); - -/** - * Validates all params and calls the right - * listener function based on its target type. - * - * @param {String|HTMLElement|HTMLCollection|NodeList} target - * @param {String} type - * @param {Function} callback - * @return {Object} - */ -function listen(target, type, callback) { - if (!target && !type && !callback) { - throw new Error('Missing required arguments'); - } - - if (!is.string(type)) { - throw new TypeError('Second argument must be a String'); - } - - if (!is.fn(callback)) { - throw new TypeError('Third argument must be a Function'); - } - - if (is.node(target)) { - return listenNode(target, type, callback); - } - else if (is.nodeList(target)) { - return listenNodeList(target, type, callback); - } - else if (is.string(target)) { - return listenSelector(target, type, callback); - } - else { - throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList'); - } -} - -/** - * Adds an event listener to a HTML element - * and returns a remove listener function. - * - * @param {HTMLElement} node - * @param {String} type - * @param {Function} callback - * @return {Object} - */ -function listenNode(node, type, callback) { - node.addEventListener(type, callback); - - return { - destroy: function() { - node.removeEventListener(type, callback); - } - } -} - -/** - * Add an event listener to a list of HTML elements - * and returns a remove listener function. - * - * @param {NodeList|HTMLCollection} nodeList - * @param {String} type - * @param {Function} callback - * @return {Object} - */ -function listenNodeList(nodeList, type, callback) { - Array.prototype.forEach.call(nodeList, function(node) { - node.addEventListener(type, callback); - }); - - return { - destroy: function() { - Array.prototype.forEach.call(nodeList, function(node) { - node.removeEventListener(type, callback); - }); - } - } -} - -/** - * Add an event listener to a selector - * and returns a remove listener function. - * - * @param {String} selector - * @param {String} type - * @param {Function} callback - * @return {Object} - */ -function listenSelector(selector, type, callback) { - return delegate(document.body, selector, type, callback); -} - -module.exports = listen; - - -/***/ }), -/* 2 */ -/***/ (function(module, exports) { - -function E () { - // Keep this empty so it's easier to inherit from - // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3) -} - -E.prototype = { - on: function (name, callback, ctx) { - var e = this.e || (this.e = {}); - - (e[name] || (e[name] = [])).push({ - fn: callback, - ctx: ctx - }); - - return this; - }, - - once: function (name, callback, ctx) { - var self = this; - function listener () { - self.off(name, listener); - callback.apply(ctx, arguments); - }; - - listener._ = callback - return this.on(name, listener, ctx); - }, - - emit: function (name) { - var data = [].slice.call(arguments, 1); - var evtArr = ((this.e || (this.e = {}))[name] || []).slice(); - var i = 0; - var len = evtArr.length; - - for (i; i < len; i++) { - evtArr[i].fn.apply(evtArr[i].ctx, data); - } - - return this; - }, - - off: function (name, callback) { - var e = this.e || (this.e = {}); - var evts = e[name]; - var liveEvents = []; - - if (evts && callback) { - for (var i = 0, len = evts.length; i < len; i++) { - if (evts[i].fn !== callback && evts[i].fn._ !== callback) - liveEvents.push(evts[i]); - } - } - - // Remove event from queue to prevent memory leak - // Suggested by https://github.com/lazd - // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910 - - (liveEvents.length) - ? e[name] = liveEvents - : delete e[name]; - - return this; - } -}; - -module.exports = E; - - -/***/ }), -/* 3 */ -/***/ (function(module, exports, __webpack_require__) { - -var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (global, factory) { - if (true) { - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [module, __webpack_require__(0), __webpack_require__(2), __webpack_require__(1)], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), - __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? - (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), - __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports !== "undefined") { - factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener')); - } else { - var mod = { - exports: {} - }; - factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener); - global.clipboard = mod.exports; - } -})(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) { - 'use strict'; - - var _clipboardAction2 = _interopRequireDefault(_clipboardAction); - - var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter); - - var _goodListener2 = _interopRequireDefault(_goodListener); - - function _interopRequireDefault(obj) { - return obj && obj.__esModule ? obj : { - default: obj - }; - } - - var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { - return typeof obj; - } : function (obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }; - - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - } - - var _createClass = function () { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); - - function _possibleConstructorReturn(self, call) { - if (!self) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return call && (typeof call === "object" || typeof call === "function") ? call : self; - } - - function _inherits(subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); - } - - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - enumerable: false, - writable: true, - configurable: true - } - }); - if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; - } - - var Clipboard = function (_Emitter) { - _inherits(Clipboard, _Emitter); - - /** - * @param {String|HTMLElement|HTMLCollection|NodeList} trigger - * @param {Object} options - */ - function Clipboard(trigger, options) { - _classCallCheck(this, Clipboard); - - var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this)); - - _this.resolveOptions(options); - _this.listenClick(trigger); - return _this; - } - - /** - * Defines if attributes would be resolved using internal setter functions - * or custom functions that were passed in the constructor. - * @param {Object} options - */ - - - _createClass(Clipboard, [{ - key: 'resolveOptions', - value: function resolveOptions() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - this.action = typeof options.action === 'function' ? options.action : this.defaultAction; - this.target = typeof options.target === 'function' ? options.target : this.defaultTarget; - this.text = typeof options.text === 'function' ? options.text : this.defaultText; - this.container = _typeof(options.container) === 'object' ? options.container : document.body; - } - }, { - key: 'listenClick', - value: function listenClick(trigger) { - var _this2 = this; - - this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) { - return _this2.onClick(e); - }); - } - }, { - key: 'onClick', - value: function onClick(e) { - var trigger = e.delegateTarget || e.currentTarget; - - if (this.clipboardAction) { - this.clipboardAction = null; - } - - this.clipboardAction = new _clipboardAction2.default({ - action: this.action(trigger), - target: this.target(trigger), - text: this.text(trigger), - container: this.container, - trigger: trigger, - emitter: this - }); - } - }, { - key: 'defaultAction', - value: function defaultAction(trigger) { - return getAttributeValue('action', trigger); - } - }, { - key: 'defaultTarget', - value: function defaultTarget(trigger) { - var selector = getAttributeValue('target', trigger); - - if (selector) { - return document.querySelector(selector); - } - } - }, { - key: 'defaultText', - value: function defaultText(trigger) { - return getAttributeValue('text', trigger); - } - }, { - key: 'destroy', - value: function destroy() { - this.listener.destroy(); - - if (this.clipboardAction) { - this.clipboardAction.destroy(); - this.clipboardAction = null; - } - } - }], [{ - key: 'isSupported', - value: function isSupported() { - var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut']; - - var actions = typeof action === 'string' ? [action] : action; - var support = !!document.queryCommandSupported; - - actions.forEach(function (action) { - support = support && !!document.queryCommandSupported(action); - }); - - return support; - } - }]); - - return Clipboard; - }(_tinyEmitter2.default); - - /** - * Helper function to retrieve attribute value. - * @param {String} suffix - * @param {Element} element - */ - function getAttributeValue(suffix, element) { - var attribute = 'data-clipboard-' + suffix; - - if (!element.hasAttribute(attribute)) { - return; - } - - return element.getAttribute(attribute); - } - - module.exports = Clipboard; -}); - -/***/ }), -/* 4 */ -/***/ (function(module, exports) { - -var DOCUMENT_NODE_TYPE = 9; - -/** - * A polyfill for Element.matches() - */ -if (typeof Element !== 'undefined' && !Element.prototype.matches) { - var proto = Element.prototype; - - proto.matches = proto.matchesSelector || - proto.mozMatchesSelector || - proto.msMatchesSelector || - proto.oMatchesSelector || - proto.webkitMatchesSelector; -} - -/** - * Finds the closest parent that matches a selector. - * - * @param {Element} element - * @param {String} selector - * @return {Function} - */ -function closest (element, selector) { - while (element && element.nodeType !== DOCUMENT_NODE_TYPE) { - if (typeof element.matches === 'function' && - element.matches(selector)) { - return element; - } - element = element.parentNode; - } -} - -module.exports = closest; - - -/***/ }), -/* 5 */ -/***/ (function(module, exports, __webpack_require__) { - -var closest = __webpack_require__(4); - -/** - * Delegates event to a selector. - * - * @param {Element} element - * @param {String} selector - * @param {String} type - * @param {Function} callback - * @param {Boolean} useCapture - * @return {Object} - */ -function _delegate(element, selector, type, callback, useCapture) { - var listenerFn = listener.apply(this, arguments); - - element.addEventListener(type, listenerFn, useCapture); - - return { - destroy: function() { - element.removeEventListener(type, listenerFn, useCapture); - } - } -} - -/** - * Delegates event to a selector. - * - * @param {Element|String|Array} [elements] - * @param {String} selector - * @param {String} type - * @param {Function} callback - * @param {Boolean} useCapture - * @return {Object} - */ -function delegate(elements, selector, type, callback, useCapture) { - // Handle the regular Element usage - if (typeof elements.addEventListener === 'function') { - return _delegate.apply(null, arguments); - } - - // Handle Element-less usage, it defaults to global delegation - if (typeof type === 'function') { - // Use `document` as the first parameter, then apply arguments - // This is a short way to .unshift `arguments` without running into deoptimizations - return _delegate.bind(null, document).apply(null, arguments); - } - - // Handle Selector-based usage - if (typeof elements === 'string') { - elements = document.querySelectorAll(elements); - } - - // Handle Array-like based usage - return Array.prototype.map.call(elements, function (element) { - return _delegate(element, selector, type, callback, useCapture); - }); -} - -/** - * Finds closest match and invokes callback. - * - * @param {Element} element - * @param {String} selector - * @param {String} type - * @param {Function} callback - * @return {Function} - */ -function listener(element, selector, type, callback) { - return function(e) { - e.delegateTarget = closest(e.target, selector); - - if (e.delegateTarget) { - callback.call(element, e); - } - } -} - -module.exports = delegate; - - -/***/ }), -/* 6 */ -/***/ (function(module, exports) { - -/** - * Check if argument is a HTML element. - * - * @param {Object} value - * @return {Boolean} - */ -exports.node = function(value) { - return value !== undefined - && value instanceof HTMLElement - && value.nodeType === 1; -}; - -/** - * Check if argument is a list of HTML elements. - * - * @param {Object} value - * @return {Boolean} - */ -exports.nodeList = function(value) { - var type = Object.prototype.toString.call(value); - - return value !== undefined - && (type === '[object NodeList]' || type === '[object HTMLCollection]') - && ('length' in value) - && (value.length === 0 || exports.node(value[0])); -}; - -/** - * Check if argument is a string. - * - * @param {Object} value - * @return {Boolean} - */ -exports.string = function(value) { - return typeof value === 'string' - || value instanceof String; -}; - -/** - * Check if argument is a function. - * - * @param {Object} value - * @return {Boolean} - */ -exports.fn = function(value) { - var type = Object.prototype.toString.call(value); - - return type === '[object Function]'; -}; - - -/***/ }), -/* 7 */ -/***/ (function(module, exports) { - -function select(element) { - var selectedText; - - if (element.nodeName === 'SELECT') { - element.focus(); - - selectedText = element.value; - } - else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { - var isReadOnly = element.hasAttribute('readonly'); - - if (!isReadOnly) { - element.setAttribute('readonly', ''); - } - - element.select(); - element.setSelectionRange(0, element.value.length); - - if (!isReadOnly) { - element.removeAttribute('readonly'); - } - - selectedText = element.value; - } - else { - if (element.hasAttribute('contenteditable')) { - element.focus(); - } - - var selection = window.getSelection(); - var range = document.createRange(); - - range.selectNodeContents(element); - selection.removeAllRanges(); - selection.addRange(range); - - selectedText = selection.toString(); - } - - return selectedText; -} - -module.exports = select; - - -/***/ }) -/******/ ]); -}); \ No newline at end of file diff --git a/addons/web/static/src/polyfills/clipboard.js b/addons/web/static/src/polyfills/clipboard.js new file mode 100644 index 00000000000..e66edb46f6c --- /dev/null +++ b/addons/web/static/src/polyfills/clipboard.js @@ -0,0 +1,100 @@ +/** @odoo-module **/ + +class ClipboardItemImpl { + constructor(items, options = {}) { + this.items = items; + this.options = options; + } + get presentationStyle() { + return this.options.presentationStyle; + } + get types() { + return Object.keys(this.items); + } + getType(type) { + return this.items[type]; + } +} + +function blobToStr(blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener("load", () => { + const { result } = reader; + if (typeof result === "string") { + resolve(result); + } else { + reject("Cannot read Blob as String"); + } + }); + reader.addEventListener("error", () => { + reject("Cannot read Blob"); + }); + reader.readAsText(blob); + }); +} + +async function stringify(item) { + const strItem = {}; + for (const type of item.types) { + strItem[type] = await blobToStr(item.getType(type)); + } + return strItem; +} + +async function write(items) { + if (!items[0].getType("text/plain")) { + throw new Error( + `Calling clipboard.write() without a "text/plain" type may result in an empty clipboard on some platforms.` + ); + } + const strItem = await stringify(items[0]); + + const stubContainer = document.createElement("div"); + const shadowContainer = stubContainer.attachShadow({ mode: "open" }); + const stub = document.createElement("span"); + stub.innerText = strItem["text/plain"]; + shadowContainer.appendChild(stub); + document.body.appendChild(stubContainer); + + const selection = document.getSelection(); + const range = document.createRange(); + range.selectNodeContents(stub); + selection.removeAllRanges(); + selection.addRange(range); + + const onCopy = (ev) => { + for (const type in strItem) { + ev.clipboardData.setData(type, strItem[type]); + } + ev.preventDefault(); + }; + document.addEventListener("copy", onCopy); + let result; + try { + result = document.execCommand("copy"); + } finally { + document.removeEventListener("copy", onCopy); + } + + selection.removeAllRanges(); + document.body.removeChild(stubContainer); + + return result; +} + +/** + * Only attempt to polyfill browsers that partially implement + * the Clipboard API (aka. Firefox with `clipboard.write()` and + * `ClipboardItem` behind a feature flag) + * + * Spec: https://w3c.github.io/clipboard-apis/ + */ +if (window.navigator.clipboard) { + if (!window.navigator.clipboard.write) { + window.navigator.clipboard.write = write.bind(window); + } + if (!window.ClipboardItem) { + window.ClipboardItem = ClipboardItemImpl; + } +} diff --git a/addons/web/static/src/views/fields/copy_clipboard/copy_button.js b/addons/web/static/src/views/fields/copy_clipboard/copy_button.js index 98aff60eea3..1f54872077e 100644 --- a/addons/web/static/src/views/fields/copy_clipboard/copy_button.js +++ b/addons/web/static/src/views/fields/copy_clipboard/copy_button.js @@ -26,9 +26,6 @@ export class CopyButton extends Component { } async onClick() { - if (!browser.navigator.clipboard) { - return browser.console.warn("This browser doesn't allow to copy to clipboard"); - } let write; // any kind of content can be copied into the clipboard using // the appropriate native methods diff --git a/addons/web/static/tests/views/fields/copy_clipboard_field_tests.js b/addons/web/static/tests/views/fields/copy_clipboard_field_tests.js index ae61cc9f9aa..31ac3e9edc0 100644 --- a/addons/web/static/tests/views/fields/copy_clipboard_field_tests.js +++ b/addons/web/static/tests/views/fields/copy_clipboard_field_tests.js @@ -195,39 +195,6 @@ QUnit.module("Fields", (hooks) => { assert.verifySteps(["copied tooltip"]); }); - QUnit.test("CopyClipboard fields with clipboard not available", async function (assert) { - patchWithCleanup(browser, { - console: { - warn: (msg) => assert.step(msg), - }, - navigator: { - clipboard: undefined, - }, - }); - - await makeView({ - serverData, - type: "form", - resModel: "partner", - arch: ` -
- -
- -
-
-
`, - resId: 1, - }); - - await click(target, ".o_clipboard_button"); - await nextTick(); - assert.verifySteps( - ["This browser doesn't allow to copy to clipboard"], - "console simply displays a warning on failure" - ); - }); - QUnit.module("CopyClipboardButtonField"); QUnit.test("CopyClipboardButtonField in form view", async function (assert) { diff --git a/addons/web/tooling/_eslintrc.json b/addons/web/tooling/_eslintrc.json index c89fbee44fa..467b2ff2ab7 100644 --- a/addons/web/tooling/_eslintrc.json +++ b/addons/web/tooling/_eslintrc.json @@ -49,7 +49,6 @@ "luxon": "readonly", "py": "readonly", "FullCalendar": "readonly", - "ClipboardJS": "readonly", "globalThis": "readonly", "Modal": "readonly", "Dropdown": "readonly", diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index f2740e33eb5..bcfeef9bf7d 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -74,7 +74,6 @@ Odoo Web Editor widget. 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', 'web/static/lib/select2/select2.js', - 'web/static/lib/clipboard/clipboard.js', 'web/static/src/legacy/js/libs/autocomplete.js', 'web/static/src/legacy/js/libs/bootstrap.js', 'web/static/src/legacy/js/libs/jquery.js', diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js b/addons/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js index 271b9bf8015..3c6d2f4e78a 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js @@ -3,6 +3,7 @@ import { _t } from "@web/core/l10n/translation"; import { ancestors } from '@web_editor/js/common/wysiwyg_utils'; import { KeepLast } from '@web/core/utils/concurrency'; +import { browser } from "@web/core/browser/browser"; export class LinkPopoverWidget { static createFor(params) { @@ -66,39 +67,12 @@ export class LinkPopoverWidget { this.$copyLink = this.$el.find('.o_we_copy_link'); this.$fullUrl = this.$el.find('.o_we_full_url'); - // Use the right ClipboardJS with respect to the prototype of this.el - // since, starting with Firefox 109, a widget element prototype that is - // adopted by an iframe will not be instanceof its original constructor. - // See: https://github.com/webcompat/web-bugs/issues/118350 - const ClipboardJS = - this.el instanceof HTMLElement - ? window.ClipboardJS - : this.el.ownerDocument.defaultView.ClipboardJS; this.$urlLink.attr('href', this.href); this.$fullUrl.attr('href', this.href); this.$el.find(`.o_we_edit_link`).on('click', this._onEditLinkClick.bind(this)); this.$el.find(`.o_we_remove_link`).on('click', this._onRemoveLinkClick.bind(this)); - // Copy onclick handler - // ClipboardJS uses "instanceof" to verify the elements passed to its - // constructor. Unfortunately, when the element is within an iframe, - // instanceof is not behaving the same across all browsers. - const containerWindow = this.container.ownerDocument.defaultView; - let _ClipboardJS = ClipboardJS; - if (this.$copyLink[0] instanceof containerWindow.HTMLElement) { - _ClipboardJS = containerWindow.ClipboardJS; - } - const clipboard = new _ClipboardJS( - this.$copyLink[0], - {text: () => this.target.href} // Absolute href - ); - clipboard.on('success', () => { - this.$copyLink.tooltip('hide'); - this.notify(_t("Link copied to clipboard."), { - type: 'success', - }); - this.popover.hide(); - }); + this.$copyLink.on("click", this._onCopyLinkClick.bind(this)); // init tooltips & popovers this.$el.find('[data-bs-toggle="tooltip"]').tooltip({ @@ -338,4 +312,19 @@ export class LinkPopoverWidget { ev.stopImmediatePropagation(); this.popover.hide(); } + /** + * Copy the link/anchor + * + * @private + * @param {Event} ev + */ + async _onCopyLinkClick(ev) { + ev.preventDefault(); + await browser.navigator.clipboard.writeText(this.target.href); + this.$copyLink.tooltip('hide'); + this.notify(_t("Link copied to clipboard."), { + type: 'success', + }); + this.popover.hide(); + } } diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index e4082706329..bfeecaf0385 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -19,6 +19,7 @@ import { convertHslToRgb, } from '@web/core/utils/colors'; import { renderToElement, renderToFragment } from "@web/core/utils/render"; +import { browser } from "@web/core/browser/browser"; import { applyTextHighlight, removeTextHighlight, @@ -2742,9 +2743,10 @@ options.registry.anchor = options.Class.extend({ * @param {Element} buttonEl */ _buildClipboard(buttonEl) { - const clipboard = new ClipboardJS(buttonEl, {text: () => this._getAnchorLink()}); - clipboard.on("success", () => { - const message = markup(_t("Anchor copied to clipboard
Link: %s", this._getAnchorLink())); + buttonEl.addEventListener("click", async (ev) => { + const anchorLink = this._getAnchorLink(); + await browser.navigator.clipboard.writeText(anchorLink); + const message = markup(_t("Anchor copied to clipboard
Link: %s", anchorLink)); this.notification.add(message, { type: "success", buttons: [{name: _t("Edit"), onClick: () => this._openAnchorDialog(buttonEl), primary: true}], diff --git a/addons/website/static/tests/tours/snippet_editor_panel_options.js b/addons/website/static/tests/tours/snippet_editor_panel_options.js index 5e947a5c4f1..7ba02d4d610 100644 --- a/addons/website/static/tests/tours/snippet_editor_panel_options.js +++ b/addons/website/static/tests/tours/snippet_editor_panel_options.js @@ -1,6 +1,7 @@ /** @odoo-module */ import wTourUtils from '@website/js/tours/tour_utils'; +import { browser } from '@web/core/browser/browser'; wTourUtils.registerWebsitePreviewTour('snippet_editor_panel_options', { test: true, @@ -50,14 +51,11 @@ wTourUtils.dragNDrop({ content: "Click on the anchor option", trigger: '#oe_snippets .snippet-option-anchor we-button', run() { - // The clipboard cannot be accessed from a script. - // https://w3c.github.io/editing/docs/execCommand/#dfn-the-copy-command - // The execCommand is patched for that step so that ClipboardJS still - // sends the 'success' event. - const oldExecCommand = document.execCommand; - document.execCommand = () => true; + // Patch and ignore write on clipboard in tour as we don't have permissions + const oldWriteText = browser.navigator.clipboard.writeText; + browser.navigator.clipboard.writeText = () => { console.info('Copy in clipboard ignored!') }; this.$anchor[0].click(); - document.execCommand = oldExecCommand; + browser.navigator.clipboard.writeText = oldWriteText; } }, { content: "Check the copied url from the notification toast", diff --git a/addons/website/static/tests/tours/snippet_popup_display_on_click.js b/addons/website/static/tests/tours/snippet_popup_display_on_click.js index 6ce96ef5152..bc81a6d5b0b 100644 --- a/addons/website/static/tests/tours/snippet_popup_display_on_click.js +++ b/addons/website/static/tests/tours/snippet_popup_display_on_click.js @@ -1,6 +1,7 @@ /** @odoo-module */ import wTourUtils from "@website/js/tours/tour_utils"; +import { browser } from "@web/core/browser/browser"; wTourUtils.registerWebsitePreviewTour("snippet_popup_display_on_click", { test: true, @@ -20,14 +21,11 @@ wTourUtils.registerWebsitePreviewTour("snippet_popup_display_on_click", { trigger: "#oe_snippets we-button[data-name='onclick_opt']", in_modal: false, run() { - // The clipboard cannot be accessed from a script. - // https://w3c.github.io/editing/docs/execCommand/#dfn-the-copy-command - // The execCommand is patched for that step so that ClipboardJS still - // sends the "success" event. - const oldExecCommand = document.execCommand; - document.execCommand = () => true; + // Patch and ignore write on clipboard in tour as we don't have permissions + const oldWriteText = browser.navigator.clipboard.writeText; + browser.navigator.clipboard.writeText = () => { console.info('Copy in clipboard ignored!') }; this.$anchor[0].click(); - document.execCommand = oldExecCommand; + browser.navigator.clipboard.writeText = oldWriteText; } }, { diff --git a/addons/website_links/static/src/js/website_links.js b/addons/website_links/static/src/js/website_links.js index 4188fadc63e..b8467b63abf 100644 --- a/addons/website_links/static/src/js/website_links.js +++ b/addons/website_links/static/src/js/website_links.js @@ -2,6 +2,7 @@ import { _t } from "@web/core/l10n/translation"; import publicWidget from "@web/legacy/js/public/public_widget"; +import { browser } from "@web/core/browser/browser"; var SelectBox = publicWidget.Widget.extend({ events: { @@ -121,13 +122,6 @@ var RecentLinkBox = publicWidget.Widget.extend({ this.animating_copy = false; this.rpc = this.bindService("rpc"); }, - /** - * @override - */ - start: function () { - new ClipboardJS(this.$('.btn_shorten_url_clipboard').get(0)); - return this._super.apply(this, arguments); - }, //-------------------------------------------------------------------------- // Private @@ -136,7 +130,9 @@ var RecentLinkBox = publicWidget.Widget.extend({ /** * @private */ - _toggleCopyButton: function () { + _toggleCopyButton: async function () { + await browser.navigator.clipboard.writeText(this.link_obj.short_url); + if (this.animating_copy) { return; } @@ -353,7 +349,7 @@ publicWidget.registry.websiteLinks = publicWidget.Widget.extend({ /** * @override */ - start: function () { + start: async function () { var defs = [this._super.apply(this, arguments)]; // UTMS selects widgets @@ -371,9 +367,6 @@ publicWidget.registry.websiteLinks = publicWidget.Widget.extend({ defs.push(this.recentLinks.appendTo($('#o_website_links_recent_links'))); this.recentLinks.getRecentLinks('newest'); - // Clipboard Library - new ClipboardJS($('#btn_shorten_url').get(0)); - this.url_copy_animating = false; $('[data-bs-toggle="tooltip"]').tooltip(); @@ -431,7 +424,10 @@ publicWidget.registry.websiteLinks = publicWidget.Widget.extend({ /** * @private */ - _onShortenUrlButtonClick: function () { + _onShortenUrlButtonClick: async function (ev) { + const textValue = ev.target.dataset["clipboard-text"]; + await browser.navigator.clipboard.writeText(textValue); + if (!$('#btn_shorten_url').hasClass('btn-copy') || this.url_copy_animating) { return; } diff --git a/addons/website_links/static/src/js/website_links_charts.js b/addons/website_links/static/src/js/website_links_charts.js index 5db29d16fcb..1768c0579fb 100644 --- a/addons/website_links/static/src/js/website_links_charts.js +++ b/addons/website_links/static/src/js/website_links_charts.js @@ -3,6 +3,7 @@ import { loadBundle } from "@web/core/assets"; import { _t } from "@web/core/l10n/translation"; import publicWidget from "@web/legacy/js/public/public_widget"; +import { browser } from "@web/core/browser/browser"; const { DateTime } = luxon; var BarChart = publicWidget.Widget.extend({ @@ -133,7 +134,7 @@ publicWidget.registry.websiteLinksCharts = publicWidget.Widget.extend({ /** * @override */ - start: function () { + start: async function () { var self = this; this.charts = {}; @@ -149,8 +150,6 @@ publicWidget.registry.websiteLinksCharts = publicWidget.Widget.extend({ defs.push(this._lastMonthClicksByCountry()); defs.push(this._super.apply(this, arguments)); - new ClipboardJS($('.copy-to-clipboard')[0]); - this.animating_copy = false; return Promise.all(defs).then(function (results) { @@ -288,9 +287,12 @@ publicWidget.registry.websiteLinksCharts = publicWidget.Widget.extend({ * @private * @param {Event} ev */ - _onCopyToClipboardClick: function (ev) { + _onCopyToClipboardClick: async function (ev) { ev.preventDefault(); + const textValue = ev.target.dataset["clipboard-text"]; + await browser.navigator.clipboard.writeText(textValue); + if (this.animating_copy) { return; } diff --git a/addons/website_links/static/src/xml/recent_link.xml b/addons/website_links/static/src/xml/recent_link.xml index 1e9f091cbe2..0c873ab8eab 100644 --- a/addons/website_links/static/src/xml/recent_link.xml +++ b/addons/website_links/static/src/xml/recent_link.xml @@ -33,7 +33,7 @@
- + Stats
diff --git a/addons/website_links/static/tests/tours/website_links.js b/addons/website_links/static/tests/tours/website_links.js index 0ba5a0280e1..820473bc3d3 100644 --- a/addons/website_links/static/tests/tours/website_links.js +++ b/addons/website_links/static/tests/tours/website_links.js @@ -1,6 +1,7 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; +import { browser } from "@web/core/browser/browser"; registry.category("web_tour.tours").add('website_links_tour', { test: true, @@ -21,7 +22,11 @@ registry.category("web_tour.tours").add('website_links_tour', { $('.o_website_links_utm_forms input#campaign-select').val(1).change(); $('.o_website_links_utm_forms input#channel-select').val(1).change(); $('.o_website_links_utm_forms input#source-select').val(1).change(); + // Patch and ignore write on clipboard in tour as we don't have permissions + const oldWriteText = browser.navigator.clipboard.writeText; + browser.navigator.clipboard.writeText = () => { console.info('Copy in clipboard ignored!') }; $('#btn_shorten_url').click(); + browser.navigator.clipboard.writeText = oldWriteText; }, }, // 2. Visit it diff --git a/addons/website_sale_loyalty/static/src/js/website_sale_gift_card.js b/addons/website_sale_loyalty/static/src/js/website_sale_gift_card.js index eab88ab664f..0b213de9af5 100644 --- a/addons/website_sale_loyalty/static/src/js/website_sale_gift_card.js +++ b/addons/website_sale_loyalty/static/src/js/website_sale_gift_card.js @@ -1,13 +1,15 @@ /** @odoo-module **/ import publicWidget from '@web/legacy/js/public/public_widget'; +import { browser } from '@web/core/browser/browser'; publicWidget.registry.WebsiteSaleGiftCardCopy = publicWidget.Widget.extend({ selector: '.o_purchased_gift_card', - /** - * @override - */ - start: function () { - new ClipboardJS(this.$el.find('.copy-to-clipboard')[0]); - } + events: { + "click .copy-to-clipboard": "_onClickCopyToClipboard", + }, + async _onClickCopyToClipboard(ev) { + const textValue = ev.target.dataset["clipboard-text"]; + await browser.navigator.clipboard.writeText(textValue); + }, }); diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index fb0cac56db9..25b04b68ca0 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -174,7 +174,6 @@ Featuring 'web/static/lib/bootstrap/js/dist/tab.js', 'web/static/lib/bootstrap/js/dist/toast.js', 'web/static/lib/select2/select2.js', - 'web/static/lib/clipboard/clipboard.js', 'web/static/src/legacy/js/libs/autocomplete.js', 'web/static/src/legacy/js/libs/bootstrap.js', 'web/static/src/legacy/js/libs/jquery.js', diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js index 755f4ac1411..34070bc535e 100644 --- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -12,6 +12,7 @@ import Dialog from '@web/legacy/js/core/dialog'; import '@website_slides/js/slides_course_join'; import { SIZES, utils as uiUtils } from "@web/core/ui/ui_service"; + import { browser } from '@web/core/browser/browser'; import { markup } from "@odoo/owl"; @@ -439,25 +440,13 @@ window.open(popUpURL, 'Share Dialog', 'width=626,height=436'); }, - _onShareLinkCopy: function (ev) { + _onShareLinkCopy: async function (ev) { ev.preventDefault(); var $clipboardBtn = this.$('.o_clipboard_button'); $clipboardBtn.tooltip({title: "Copied!", trigger: "manual", placement: "bottom"}); - var self = this; - var clipboard = new ClipboardJS('.o_clipboard_button', { - target: function () { - return self.$('.o_wslides_js_share_link')[0]; - }, - container: this.el - }); - clipboard.on('success', function () { - clipboard.destroy(); - $clipboardBtn.tooltip('show'); - setTimeout(() => $clipboardBtn.tooltip("hide"), 800); - }); - clipboard.on('error', function (e) { - clipboard.destroy(); - }) + await browser.navigator.clipboard.writeText(this.$('.o_wslides_js_share_link')[0].innerText); + $clipboardBtn.tooltip('show'); + setTimeout(() => $clipboardBtn.tooltip("hide"), 800); }, }); diff --git a/addons/website_slides/static/src/js/slides_share.js b/addons/website_slides/static/src/js/slides_share.js index 2afc5750e26..f7c5fd7751e 100644 --- a/addons/website_slides/static/src/js/slides_share.js +++ b/addons/website_slides/static/src/js/slides_share.js @@ -3,6 +3,7 @@ import publicWidget from '@web/legacy/js/public/public_widget'; import '@website_slides/js/slides'; import { _t } from "@web/core/l10n/translation"; +import { browser } from '@web/core/browser/browser'; export const ShareMail = publicWidget.Widget.extend({ events: { @@ -109,27 +110,14 @@ publicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({ }); }, - _onShareLinkCopy: function (ev) { + _onShareLinkCopy: async function (ev) { ev.preventDefault(); var $clipboardBtn = $(ev.currentTarget); $clipboardBtn.tooltip({title: "Copied!", trigger: "manual", placement: "bottom"}); - var self = this; - var clipboard = new ClipboardJS('#' + $clipboardBtn[0].id, { - target: function () { - var share_link_el = self.$('#wslides_share_link_id_' + $clipboardBtn[0].id.split('id_')[1]); - return share_link_el[0]; - }, - container: this.el - }); - clipboard.on('success', function () { - clipboard.destroy(); - $clipboardBtn.tooltip('show'); - setTimeout(() => $clipboardBtn.tooltip("hide"), 800); - }); - clipboard.on('error', function (e) { - console.log(e); - clipboard.destroy(); - }) + var share_link_el = this.$('#wslides_share_link_id_' + $clipboardBtn[0].id.split('id_')[1]); + await browser.navigator.clipboard.writeText(share_link_el[0].innerText); + $clipboardBtn.tooltip('show'); + setTimeout(() => $clipboardBtn.tooltip("hide"), 800); }, }); @@ -139,28 +127,16 @@ publicWidget.registry.websiteSlidesEmbedShare = publicWidget.Widget.extend({ 'click .o_embed_clipboard_button': '_onShareLinkCopy', }, - _onShareLinkCopy: function (ev) { + _onShareLinkCopy: async function (ev) { ev.preventDefault(); const $clipboardBtn = $(ev.currentTarget); $clipboardBtn.tooltip({title: "Copied!", trigger: "manual", placement: "bottom"}); - const clipboard = new ClipboardJS('#' + $clipboardBtn[0].id, { - target: () => { - const share_embed_el = this.$('#wslides_share_embed_id_' + $clipboardBtn[0].id.split('id_')[1]); - return share_embed_el[0]; - }, - container: this.el - }); - clipboard.on('success', function () { - clipboard.destroy(); - $clipboardBtn.tooltip('show'); - setTimeout(function () { - $clipboardBtn.tooltip("hide"); - }, 800); - }); - clipboard.on('error', function (e) { - console.log(e); - clipboard.destroy(); - }) + var share_embed_el = this.$('#wslides_share_embed_id_' + $clipboardBtn[0].id.split('id_')[1]); + await browser.navigator.clipboard.writeText(share_embed_el[0].innerText); + $clipboardBtn.tooltip('show'); + setTimeout(function () { + $clipboardBtn.tooltip("hide"); + }, 800); }, }); diff --git a/debian/copyright b/debian/copyright index 4c585020654..3a6f2c10a9f 100644 --- a/debian/copyright +++ b/debian/copyright @@ -229,10 +229,6 @@ Files: addons/web/static/lib/bootstrap/scss/_custom-forms.scss Copyright: 2014 Waybury License: MIT -Files: addons/web/static/lib/clipboard/clipboard.js -Copyright: Zeno Rocha -License: MIT - Files: addons/web/static/lib/es6-promise/* Copyright: 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors License: MIT diff --git a/odoo/addons/test_lint/tests/eslintrc b/odoo/addons/test_lint/tests/eslintrc index 8186bfa7d9d..637750068d5 100644 --- a/odoo/addons/test_lint/tests/eslintrc +++ b/odoo/addons/test_lint/tests/eslintrc @@ -34,7 +34,6 @@ "luxon": "readonly", "py": "readonly", "FullCalendar": "readonly", - "ClipboardJS": "readonly", "Modal": "readonly", "Dropdown": "readonly", "ScrollSpy": "readonly",