diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/libs | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/libs')
| -rw-r--r-- | addons/web/static/src/js/libs/autocomplete.js | 38 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/bootstrap.js | 131 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/content-disposition.js | 249 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/daterangepicker.js | 24 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/download.js | 153 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/fullcalendar.js | 252 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/jquery.js | 235 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/pdfjs.js | 20 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/popper.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/underscore.js | 52 | ||||
| -rw-r--r-- | addons/web/static/src/js/libs/zoomodoo.js | 353 |
11 files changed, 1509 insertions, 0 deletions
diff --git a/addons/web/static/src/js/libs/autocomplete.js b/addons/web/static/src/js/libs/autocomplete.js new file mode 100644 index 00000000..72f3ee89 --- /dev/null +++ b/addons/web/static/src/js/libs/autocomplete.js @@ -0,0 +1,38 @@ +odoo.define('web.autocomplete.extensions', function () { +'use strict'; + +/** + * The jquery autocomplete library extensions and fixes should be done here to + * avoid patching in place. + */ + +// jquery autocomplete tweak to allow html and classnames +var proto = $.ui.autocomplete.prototype; +var initSource = proto._initSource; + +function filter( array, term ) { + var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); + return $.grep( array, function (value_) { + return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() ); + }); +} + +$.extend(proto, { + _initSource: function () { + if ( this.options.html && $.isArray(this.options.source) ) { + this.source = function (request, response) { + response( filter( this.options.source, request.term ) ); + }; + } else { + initSource.call( this ); + } + }, + _renderItem: function (ul, item) { + return $( "<li></li>" ) + .data( "item.autocomplete", item ) + .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) ) + .appendTo( ul ) + .addClass(item.classname); + }, +}); +}); diff --git a/addons/web/static/src/js/libs/bootstrap.js b/addons/web/static/src/js/libs/bootstrap.js new file mode 100644 index 00000000..9854b47a --- /dev/null +++ b/addons/web/static/src/js/libs/bootstrap.js @@ -0,0 +1,131 @@ +odoo.define('web.bootstrap.extensions', function () { +'use strict'; + +/** + * The bootstrap library extensions and fixes should be done here to avoid + * patching in place. + */ + +/** + * Review Bootstrap Sanitization: leave it enabled by default but extend it to + * accept more common tag names like tables and buttons, and common attributes + * such as style or data-. If a specific tooltip or popover must accept custom + * tags or attributes, they must be supplied through the whitelist BS + * parameter explicitely. + * + * We cannot disable sanitization because bootstrap uses tooltip/popover + * DOM attributes in an "unsafe" way. + */ +var bsSanitizeWhiteList = $.fn.tooltip.Constructor.Default.whiteList; + +bsSanitizeWhiteList['*'].push('title', 'style', /^data-[\w-]+/); + +bsSanitizeWhiteList.header = []; +bsSanitizeWhiteList.main = []; +bsSanitizeWhiteList.footer = []; + +bsSanitizeWhiteList.caption = []; +bsSanitizeWhiteList.col = ['span']; +bsSanitizeWhiteList.colgroup = ['span']; +bsSanitizeWhiteList.table = []; +bsSanitizeWhiteList.thead = []; +bsSanitizeWhiteList.tbody = []; +bsSanitizeWhiteList.tfooter = []; +bsSanitizeWhiteList.tr = []; +bsSanitizeWhiteList.th = ['colspan', 'rowspan']; +bsSanitizeWhiteList.td = ['colspan', 'rowspan']; + +bsSanitizeWhiteList.address = []; +bsSanitizeWhiteList.article = []; +bsSanitizeWhiteList.aside = []; +bsSanitizeWhiteList.blockquote = []; +bsSanitizeWhiteList.section = []; + +bsSanitizeWhiteList.button = ['type']; +bsSanitizeWhiteList.del = []; + +/** + * Returns an extended version of bootstrap default whitelist for sanitization, + * i.e. a version where, for each key, the original value is concatened with the + * received version's value and where the received version's extra key/values + * are added. + * + * Note: the returned version + * + * @param {Object} extensions + * @returns {Object} /!\ the returned whitelist is made from a *shallow* copy of + * the default whitelist, extended with given whitelist. + */ +function makeExtendedSanitizeWhiteList(extensions) { + var whiteList = _.clone($.fn.tooltip.Constructor.Default.whiteList); + Object.keys(extensions).forEach(key => { + whiteList[key] = (whiteList[key] || []).concat(extensions[key]); + }); + return whiteList; +} + +/* Bootstrap tooltip defaults overwrite */ +$.fn.tooltip.Constructor.Default.placement = 'auto'; +$.fn.tooltip.Constructor.Default.fallbackPlacement = ['bottom', 'right', 'left', 'top']; +$.fn.tooltip.Constructor.Default.html = true; +$.fn.tooltip.Constructor.Default.trigger = 'hover'; +$.fn.tooltip.Constructor.Default.container = 'body'; +$.fn.tooltip.Constructor.Default.boundary = 'window'; +$.fn.tooltip.Constructor.Default.delay = { show: 1000, hide: 0 }; + +var bootstrapShowFunction = $.fn.tooltip.Constructor.prototype.show; +$.fn.tooltip.Constructor.prototype.show = function () { + // Overwrite bootstrap tooltip method to prevent showing 2 tooltip at the + // same time + $('.tooltip').remove(); + + return bootstrapShowFunction.call(this); +}; + +/* Bootstrap scrollspy fix for non-body to spy */ + +const bootstrapSpyRefreshFunction = $.fn.scrollspy.Constructor.prototype.refresh; +$.fn.scrollspy.Constructor.prototype.refresh = function () { + bootstrapSpyRefreshFunction.apply(this, arguments); + if (this._scrollElement === window || this._config.method !== 'offset') { + return; + } + const baseScrollTop = this._getScrollTop(); + for (let i = 0; i < this._offsets.length; i++) { + this._offsets[i] += baseScrollTop; + } +}; + +/** + * In some cases, we need to keep the first element of navbars selected. + */ +const bootstrapSpyProcessFunction = $.fn.scrollspy.Constructor.prototype._process; +$.fn.scrollspy.Constructor.prototype._process = function () { + bootstrapSpyProcessFunction.apply(this, arguments); + if (this._activeTarget === null && this._config.alwaysKeepFirstActive) { + this._activate(this._targets[0]); + } +}; + +/* Bootstrap modal scrollbar compensation on non-body */ +const bsSetScrollbarFunction = $.fn.modal.Constructor.prototype._setScrollbar; +$.fn.modal.Constructor.prototype._setScrollbar = function () { + const $scrollable = $().getScrollingElement(); + if (document.body.contains($scrollable[0])) { + $scrollable.compensateScrollbar(true); + } + return bsSetScrollbarFunction.apply(this, arguments); +}; +const bsResetScrollbarFunction = $.fn.modal.Constructor.prototype._resetScrollbar; +$.fn.modal.Constructor.prototype._resetScrollbar = function () { + const $scrollable = $().getScrollingElement(); + if (document.body.contains($scrollable[0])) { + $scrollable.compensateScrollbar(false); + } + return bsResetScrollbarFunction.apply(this, arguments); +}; + +return { + makeExtendedSanitizeWhiteList: makeExtendedSanitizeWhiteList, +}; +}); diff --git a/addons/web/static/src/js/libs/content-disposition.js b/addons/web/static/src/js/libs/content-disposition.js new file mode 100644 index 00000000..d229bd4c --- /dev/null +++ b/addons/web/static/src/js/libs/content-disposition.js @@ -0,0 +1,249 @@ +/* +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * Stripped down to only parsing/decoding. + */ +odoo.define('web.contentdisposition', function () { +'use strict'; + +/** + * RegExp to match percent encoding escape. + * @private + */ +var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g; + +/** + * RegExp to match non-latin1 characters. + * @private + */ +var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g; + +/** + * RegExp to match quoted-pair in RFC 2616 + * + * quoted-pair = "\" CHAR + * CHAR = <any US-ASCII character (octets 0 - 127)> + * @private + */ +var QESC_REGEXP = /\\([\u0000-\u007f])/g; + +/** + * RegExp for various RFC 2616 grammar + * + * parameter = token "=" ( token | quoted-string ) + * token = 1*<any CHAR except CTLs or separators> + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + * qdtext = <any TEXT except <">> + * quoted-pair = "\" CHAR + * CHAR = <any US-ASCII character (octets 0 - 127)> + * TEXT = <any OCTET except CTLs, but including LWS> + * LWS = [CRLF] 1*( SP | HT ) + * CRLF = CR LF + * CR = <US-ASCII CR, carriage return (13)> + * LF = <US-ASCII LF, linefeed (10)> + * SP = <US-ASCII SP, space (32)> + * HT = <US-ASCII HT, horizontal-tab (9)> + * CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> + * OCTET = <any 8-bit sequence of data> + * @private + */ +var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g; + +/** + * RegExp for various RFC 5987 grammar + * + * ext-value = charset "'" [ language ] "'" value-chars + * charset = "UTF-8" / "ISO-8859-1" / mime-charset + * mime-charset = 1*mime-charsetc + * mime-charsetc = ALPHA / DIGIT + * / "!" / "#" / "$" / "%" / "&" + * / "+" / "-" / "^" / "_" / "`" + * / "{" / "}" / "~" + * language = ( 2*3ALPHA [ extlang ] ) + * / 4ALPHA + * / 5*8ALPHA + * extlang = *3( "-" 3ALPHA ) + * value-chars = *( pct-encoded / attr-char ) + * pct-encoded = "%" HEXDIG HEXDIG + * attr-char = ALPHA / DIGIT + * / "!" / "#" / "$" / "&" / "+" / "-" / "." + * / "^" / "_" / "`" / "|" / "~" + * @private + */ +var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/; + +/** + * RegExp for various RFC 6266 grammar + * + * disposition-type = "inline" | "attachment" | disp-ext-type + * disp-ext-type = token + * disposition-parm = filename-parm | disp-ext-parm + * filename-parm = "filename" "=" value + * | "filename*" "=" ext-value + * disp-ext-parm = token "=" value + * | ext-token "=" ext-value + * ext-token = <the characters in token, followed by "*"> + * @private + */ +var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/; + +/** + * Decode a RFC 6987 field value (gracefully). + * + * @param {string} str + * @return {string} + * @private + */ +function decodefield(str) { + var match = EXT_VALUE_REGEXP.exec(str); + + if (!match) { + throw new TypeError('invalid extended field value') + } + + var charset = match[1].toLowerCase(); + var encoded = match[2]; + + switch (charset) { + case 'iso-8859-1': + return encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode).replace(NON_LATIN1_REGEXP, '?'); + case 'utf-8': + return decodeURIComponent(encoded); + default: + throw new TypeError('unsupported charset in extended field') + } +} + +/** + * Parse Content-Disposition header string. + * + * @param {string} string + * @return {ContentDisposition} + * @public + */ +function parse(string) { + if (!string || typeof string !== 'string') { + throw new TypeError('argument string is required') + } + + var match = DISPOSITION_TYPE_REGEXP.exec(string); + + if (!match) { + throw new TypeError('invalid type format') + } + + // normalize type + var index = match[0].length; + var type = match[1].toLowerCase(); + + var key; + var names = []; + var params = {}; + var value; + + // calculate index to start at + index = PARAM_REGEXP.lastIndex = match[0].substr(-1) === ';' ? index - 1 : index; + + // match parameters + while ((match = PARAM_REGEXP.exec(string))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length; + key = match[1].toLowerCase(); + value = match[2]; + + if (names.indexOf(key) !== -1) { + throw new TypeError('invalid duplicate parameter') + } + + names.push(key); + + if (key.indexOf('*') + 1 === key.length) { + // decode extended value + key = key.slice(0, -1); + value = decodefield(value); + + // overwrite existing value + params[key] = value; + continue + } + + if (typeof params[key] === 'string') { + continue + } + + if (value[0] === '"') { + // remove quotes and escapes + value = value + .substr(1, value.length - 2) + .replace(QESC_REGEXP, '$1') + } + + params[key] = value + } + + if (index !== -1 && index !== string.length) { + throw new TypeError('invalid parameter format') + } + + return new ContentDisposition(type, params) +} + +/** + * Percent decode a single character. + * + * @param {string} str + * @param {string} hex + * @return {string} + * @private + */ +function pdecode(str, hex) { + return String.fromCharCode(parseInt(hex, 16)) +} + +/** + * Class for parsed Content-Disposition header for v8 optimization + * + * @public + * @param {string} type + * @param {object} parameters + * @constructor + */ +function ContentDisposition(type, parameters) { + this.type = type; + this.parameters = parameters +} + +return { + parse: parse, +}; +}); diff --git a/addons/web/static/src/js/libs/daterangepicker.js b/addons/web/static/src/js/libs/daterangepicker.js new file mode 100644 index 00000000..dd731eb4 --- /dev/null +++ b/addons/web/static/src/js/libs/daterangepicker.js @@ -0,0 +1,24 @@ +odoo.define('web.daterangepicker.extensions', function () { +'use strict'; + +/** + * Don't allow user to select off days(Dates which are out of current calendar). + */ +var clickDateFunction = daterangepicker.prototype.clickDate; +daterangepicker.prototype.clickDate = function (ev) { + if (!$(ev.target).hasClass('off')) { + clickDateFunction.apply(this, arguments); + } +}; + +/** + * Override to open up or down based on top/bottom space in window. + */ +const moveFunction = daterangepicker.prototype.move; +daterangepicker.prototype.move = function () { + const offset = this.element.offset(); + this.drops = this.container.height() < offset.top ? 'up' : 'down'; + moveFunction.apply(this, arguments); +}; + +}); diff --git a/addons/web/static/src/js/libs/download.js b/addons/web/static/src/js/libs/download.js new file mode 100644 index 00000000..97a015f0 --- /dev/null +++ b/addons/web/static/src/js/libs/download.js @@ -0,0 +1,153 @@ +/* +MIT License + +Copyright (c) 2016 dandavis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + */ + +/** + * download.js v4.2, by dandavis; 2008-2018. [MIT] see http://danml.com/download.html for tests/usage + * + * @param {Blob | File | String} data + * @param {String} [filename] + * @param {String} [mimetype] + */ +odoo.define('web.download', function () { +return function download(data, filename, mimetype) { + var self = window, // this script is only for browsers anyway... + defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads + mimeType = mimetype || defaultMime, payload = data, + url = !filename && !mimetype && payload, + anchor = document.createElement("a"), + toString = function (a) {return String(a);}, + myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString), + fileName = filename || "download", blob, reader; + myBlob = myBlob.call ? myBlob.bind(self) : Blob; + + if (String(this) === "true") { //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback + payload = [payload, mimeType]; + mimeType = payload[0]; + payload = payload[1]; + } + + if (url && url.length < 2048) { // if no filename and no mime, assume a url was passed as the only argument + fileName = url.split("/").pop().split("?")[0]; + anchor.href = url; // assign href prop to temp anchor + if (anchor.href.indexOf(url) !== -1) { // if the browser determines that it's a potentially valid url path: + var ajax = new XMLHttpRequest(); + ajax.open("GET", url, true); + ajax.responseType = 'blob'; + ajax.onload = function (e) { + download(e.target.response, fileName, defaultMime); + }; + setTimeout(function () { ajax.send();}, 0); // allows setting custom ajax headers using the return: + return ajax; + } + } + + //go ahead and download dataURLs right away + if (/^data:[\w+\-]+\/[\w+\-]+[,;]/.test(payload)) { + + if (payload.length > (1024 * 1024 * 1.999) && myBlob !== toString) { + payload = dataUrlToBlob(payload); + mimeType = payload.type || defaultMime; + } else { + return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs: + navigator.msSaveBlob(dataUrlToBlob(payload), fileName) : saver(payload); // everyone else can save dataURLs un-processed + } + + } + + blob = payload instanceof myBlob ? payload : new myBlob([payload], {type: mimeType}); + + + function dataUrlToBlob(strUrl) { + var parts = strUrl.split(/[:;,]/), type = parts[1], + decoder = parts[2] === "base64" ? atob : decodeURIComponent, + binData = decoder(parts.pop()), mx = binData.length, + i = 0, uiArr = new Uint8Array(mx); + + for (i; i < mx; ++i) uiArr[i] = binData.charCodeAt(i); + + return new myBlob([uiArr], {type: type}); + } + + function saver(url, winMode) { + if ('download' in anchor) { //html5 A[download] + anchor.href = url; + anchor.setAttribute("download", fileName); + anchor.className = "download-js-link"; + anchor.innerHTML = "downloading..."; + anchor.style.display = "none"; + document.body.appendChild(anchor); + setTimeout(function () { + anchor.click(); + document.body.removeChild(anchor); + if (winMode === true) {setTimeout(function () { self.URL.revokeObjectURL(anchor.href);}, 250);} + }, 66); + return true; + } + + // handle non-a[download] safari as best we can: + if (/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) { + url = url.replace(/^data:([\w\/\-+]+)/, defaultMime); + if (!window.open(url)) { // popup blocked, offer direct download: + if (confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")) { location.href = url; } + } + return true; + } + + //do iframe dataURL download (old ch+FF): + var f = document.createElement("iframe"); + document.body.appendChild(f); + + if (!winMode) { // force a mime that will download: + url = "data:" + url.replace(/^data:([\w\/\-+]+)/, defaultMime); + } + f.src = url; + setTimeout(function () { document.body.removeChild(f); }, 333); + } + + if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL) + return navigator.msSaveBlob(blob, fileName); + } + + if (self.URL) { // simple fast and modern way using Blob and URL: + saver(self.URL.createObjectURL(blob), true); + } else { + // handle non-Blob()+non-URL browsers: + if (typeof blob === "string" || blob.constructor === toString) { + try { + return saver("data:" + mimeType + ";base64," + self.btoa(blob)); + } catch (y) { + return saver("data:" + mimeType + "," + encodeURIComponent(blob)); + } + } + + // Blob but not URL support: + reader = new FileReader(); + reader.onload = function () { + saver(this.result); + }; + reader.readAsDataURL(blob); + } + return true; +}; +}); diff --git a/addons/web/static/src/js/libs/fullcalendar.js b/addons/web/static/src/js/libs/fullcalendar.js new file mode 100644 index 00000000..7f9714e8 --- /dev/null +++ b/addons/web/static/src/js/libs/fullcalendar.js @@ -0,0 +1,252 @@ +odoo.define('/web/static/src/js/libs/fullcalendar.js', function () { + "use strict"; + + function createYearCalendarView(FullCalendar) { + const { + Calendar, + createElement, + EventApi, + memoizeRendering, + View, + } = FullCalendar; + + class YearView extends View { + constructor() { + super(...arguments); + this.months = null; + this.renderSubCalendarsMem = memoizeRendering( + this.renderSubCalendars, this.unrenderSubCalendars); + this.events = []; + } + + //---------------------------------------------------------------------- + // Getters + //---------------------------------------------------------------------- + + get currentDate() { + return this.context.calendar.state.currentDate; + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + */ + destroy() { + this.renderSubCalendarsMem.unrender(); + super.destroy(); + } + /** + * Removes the selection on sub calendar. + * Selections on sub calendars are not propagated to this view so + * this view cannot manage them. + */ + unselect() { + for (const { calendar } of this.months) { + calendar.unselect(); + } + } + /** + * @override + */ + render() { + this.renderSubCalendarsMem(this.context); + super.render(...arguments); + } + /** + * Renders the main layout (the 4x3 month grid) + */ + renderSubCalendars() { + this.el.classList.add('fc-scroller'); + if (!this.context.options.selectable) { + this.el.classList.add('fc-readonly-year-view'); + } + this.months = []; + for (let monthNumber = 0; monthNumber < 12; monthNumber++) { + const monthDate = new Date(this.currentDate.getFullYear(), monthNumber); + const monthShortName = moment(monthDate).format('MMM').toLowerCase(); + const container = createElement('div', { class: 'fc-month-container' }); + this.el.appendChild(container); + const el = createElement('div', { + class: `fc-month fc-month-${monthShortName}`, + }); + container.appendChild(el); + const calendar = this._createMonthCalendar(el, monthDate); + this.months.push({ el, calendar }); + calendar.render(); + } + } + /** + * Removes the main layout (the 4x3 month grid). + * Called when view is switched/destroyed. + */ + unrenderSubCalendars() { + for (const { el, calendar } of this.months) { + calendar.destroy(); + el.remove(); + } + } + /** + * Renders events in sub calendars. + * Called every time event source changed (when changing the date, + * when changing filters, adding/removing filters). + */ + renderEvents() { + // `renderDates` also renders events so if it's called just before + // then do not execute this as it will do a re-render. + if (this.datesRendered) { + this.datesRendered = false; + return; + } + this.events = this._computeEvents(); + for (const { calendar } of this.months) { + calendar.refetchEvents(); + } + this._setCursorOnEventDates(); + } + /** + * Renders dates and events in sub calendars. + * Called when the year of the date changed to render a new + * 4*3 grid of month calendar based on the new year. + */ + renderDates() { + this.events = this._computeEvents(); + for (const [monthNumber, { calendar }] of Object.entries(this.months)) { + const monthDate = new Date(this.currentDate.getFullYear(), monthNumber); + calendar.gotoDate(monthDate); + } + this._setCursorOnEventDates(); + this.datesRendered = true; + } + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @private + */ + _computeEvents() { + const calendar = this.context.calendar; + return calendar.getEvents().map(event => { + const endUTC = calendar.dateEnv.toDate(event._instance.range.end); + const end = new Date(event._instance.range.end); + if (endUTC.getHours() > 0 || endUTC.getMinutes() > 0 || + endUTC.getSeconds() > 0 || endUTC.getMilliseconds() > 0) { + end.setDate(end.getDate() + 1); + } + // clone event data to not trigger rerendering and issues + const instance = Object.assign({}, event._instance, { + range: { start: new Date(event._instance.range.start), end }, + }); + const def = Object.assign({}, event._def, { + rendering: 'background', + allDay: true, + }); + return new EventApi(this.context.calendar, def, instance); + }); + } + /** + * Create a month calendar for the date `monthDate` and mount it on container. + * + * @private + * @param {HTMLElement} container + * @param {Date} monthDate + */ + _createMonthCalendar(container, monthDate) { + return new Calendar(container, Object.assign({}, this.context.options, { + defaultDate: monthDate, + defaultView: 'dayGridMonth', + header: { left: false, center: 'title', right: false }, + titleFormat: { month: 'short', year: 'numeric' }, + height: 0, + contentHeight: 0, + weekNumbers: false, + showNonCurrentDates: false, + views: { + dayGridMonth: { + columnHeaderText: (date) => moment(date).format("ddd")[0], + }, + }, + selectMinDistance: 5, // needed to not trigger select when click + dateClick: this._onYearDateClick.bind(this), + datesRender: undefined, + events: (info, successCB) => { + successCB(this.events); + }, + windowResize: undefined, + })); + } + /** + * Sets fc-has-event class on every dates that have at least one event. + * + * @private + */ + _setCursorOnEventDates() { + for (const el of this.el.querySelectorAll('.fc-has-event')) { + el.classList.remove('fc-has-event'); + } + for (const event of Object.values(this.events)) { + let currentDate = moment(event._instance.range.start); + while (currentDate.isBefore(event._instance.range.end, 'day')) { + const formattedDate = currentDate.format('YYYY-MM-DD'); + const el = this.el.querySelector(`.fc-day-top[data-date="${formattedDate}"]`); + if (el) { + el.classList.add('fc-has-event'); + } + currentDate.add(1, 'days'); + } + } + } + + //---------------------------------------------------------------------- + // Handlers + //---------------------------------------------------------------------- + + /** + * @private + * @param {*} info + */ + _onYearDateClick(info) { + const calendar = this.context.calendar; + const events = Object.values(this.events) + .filter(event => { + const startUTC = calendar.dateEnv.toDate(event._instance.range.start); + const endUTC = calendar.dateEnv.toDate(event._instance.range.end); + const start = moment(startUTC); + const end = moment(endUTC); + const inclusivity = start.isSame(end, 'day') ? '[]' : '[)'; + return moment(info.date).isBetween(start, end, 'day', inclusivity); + }) + .map(event => { + return Object.assign({}, event._def, event._instance.range); + }); + const yearDateInfo = Object.assign({}, info, { + view: this, + monthView: info.view, + events, + selectable: this.context.options.selectable, + }); + calendar.publiclyTrigger('yearDateClick', [yearDateInfo]); + } + } + + return FullCalendar.createPlugin({ + views: { + dayGridYear: { + class: YearView, + duration: { years: 1 }, + defaults: { + fixedWeekCount: true, + }, + }, + }, + }); + } + + return { + createYearCalendarView, + }; +}); diff --git a/addons/web/static/src/js/libs/jquery.js b/addons/web/static/src/js/libs/jquery.js new file mode 100644 index 00000000..749bcd80 --- /dev/null +++ b/addons/web/static/src/js/libs/jquery.js @@ -0,0 +1,235 @@ +odoo.define('web.jquery.extensions', function () { +'use strict'; + +/** + * The jquery library extensions and fixes should be done here to avoid patching + * in place. + */ + +// jQuery selectors extensions +$.extend($.expr[':'], { + containsLike: function (element, index, matches){ + return element.innerHTML.toUpperCase().indexOf(matches[3].toUpperCase()) >= 0; + }, + containsTextLike: function (element, index, matches){ + return element.innerText.toUpperCase().indexOf(matches[3].toUpperCase()) >= 0; + }, + containsExact: function (element, index, matches){ + return $.trim(element.innerHTML) === matches[3]; + }, + containsExactText: function (element, index, matches) { + return element.innerText.trim() === matches[3].trim(); + }, + /** + * Note all escaped characters need to be double escaped inside of the + * expression, so "\(" needs to be "\\(" + */ + containsRegex: function (element, index, matches){ + var regreg = /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})$/, + reg = regreg.exec(matches[3]); + return reg ? new RegExp(reg[1], reg[2]).test($.trim(element.innerHTML)) : false; + }, + propChecked: function (element, index, matches) { + return $(element).prop("checked") === true; + }, + propSelected: function (element, index, matches) { + return $(element).prop("selected") === true; + }, + propValue: function (element, index, matches) { + return $(element).prop("value") === matches[3]; + }, + propValueContains: function (element, index, matches) { + return $(element).prop("value") && $(element).prop("value").indexOf(matches[3]) !== -1; + }, + hasData: function (element) { + return !!_.toArray(element.dataset).length; + }, + data: function (element, index, matches) { + return $(element).data(matches[3]); + }, + hasVisibility: function (element, index, matches) { + var $element = $(element); + if ($(element).css('visibility') === 'hidden') { + return false; + } + var $parent = $element.parent(); + if (!$parent.length || $element.is('html')) { + return true; + } + return $parent.is(':hasVisibility'); + }, + hasOpacity: function (element, index, matches) { + var $element = $(element); + if (parseFloat($(element).css('opacity')) <= 0.01) { + return false; + } + var $parent = $element.parent(); + if (!$parent.length || $element.is('html')) { + return true; + } + return $parent.is(':hasOpacity'); + }, +}); + +// jQuery functions extensions +$.fn.extend({ + /** + * Returns all the attributes of a DOM element (first one in the jQuery + * set). + * + * @returns {Object} attribute name -> attribute value + */ + getAttributes: function () { + var o = {}; + if (this.length) { + var attrs = this[0].attributes; + for (var i = 0, l = attrs.length ; i < l ; i++) { + var attr = attrs.item(i); + o[attr.name] = attr.value; + } + } + return o; + }, + /** + * Makes DOM elements bounce the way Odoo decided it. + * + * @param {string} [extraClass] + */ + odooBounce: function (extraClass) { + for (const el of this) { + el.classList.add('o_catch_attention', extraClass); + setTimeout(() => el.classList.remove('o_catch_attention', extraClass), 400); + } + return this; + }, + /** + * Allows to bind events to a handler just as the standard `$.on` function + * but binds the handler so that it is executed before any already-attached + * handler for the same events. + * + * @see jQuery.on + */ + prependEvent: function (events, selector, data, handler) { + this.on.apply(this, arguments); + + events = events.split(' '); + return this.each(function () { + var el = this; + _.each(events, function (evNameNamespaced) { + var evName = evNameNamespaced.split('.')[0]; + var handler = $._data(el, 'events')[evName].pop(); + $._data(el, 'events')[evName].unshift(handler); + }); + }); + }, + /** + * @return {jQuery} + */ + closestScrollable() { + let $el = this; + while ($el[0] !== document.scrollingElement) { + if ($el.isScrollable()) { + return $el; + } + $el = $el.parent(); + } + return $el; + }, + /** + * Adapt the given css property by adding the size of a scrollbar if any. + * Limitation: only works if the given css property is not already used as + * inline style for another reason. + * + * @param {boolean} [add=true] + * @param {boolean} [isScrollElement=true] + * @param {string} [cssProperty='padding-right'] + */ + compensateScrollbar(add = true, isScrollElement = true, cssProperty = 'padding-right') { + for (const el of this) { + // Compensate scrollbar + el.style.removeProperty(cssProperty); + if (!add) { + return; + } + const scrollableEl = isScrollElement ? el : $(el).parent().closestScrollable()[0]; + const style = window.getComputedStyle(el); + const newValue = parseInt(style[cssProperty]) + scrollableEl.offsetWidth - scrollableEl.clientWidth; + el.style.setProperty(cssProperty, `${newValue}px`, 'important'); + } + }, + /** + * @returns {jQuery} + */ + getScrollingElement() { + const $baseScrollingElement = $(document.scrollingElement); + if ($baseScrollingElement.isScrollable() + && $baseScrollingElement.hasScrollableContent()) { + return $baseScrollingElement; + } + const bodyHeight = $(document.body).height(); + for (const el of document.body.children) { + // Search for a body child which is at least as tall as the body + // and which has the ability to scroll if enough content in it. If + // found, suppose this is the top scrolling element. + if (bodyHeight - el.scrollHeight > 1.5) { + continue; + } + const $el = $(el); + if ($el.isScrollable()) { + return $el; + } + } + return $baseScrollingElement; + }, + /** + * @return {boolean} + */ + hasScrollableContent() { + return this[0].scrollHeight > this[0].clientHeight; + }, + /** + * @returns {boolean} + */ + isScrollable() { + const overflow = this.css('overflow-y'); + return overflow === 'auto' || overflow === 'scroll' + || (overflow === 'visible' && this === document.scrollingElement); + }, +}); + +// jQuery functions monkey-patching + +// Some magic to ensure scrolltop and animate on html/body animate the top level +// scrollable element even if not html or body. +const originalScrollTop = $.fn.scrollTop; +$.fn.scrollTop = function (value) { + if (value !== undefined && this.filter('html, body').length) { + // The caller wants to scroll a set of elements including html and/or + // body to a specific point -> do that but make sure to add the real + // top level element to that set of elements if any different is found. + originalScrollTop.apply(this.not('html, body').add($().getScrollingElement()), arguments); + return this; + } else if (value === undefined && this.eq(0).is('html, body')) { + // The caller wants to get the scroll point of a set of elements, jQuery + // will return the scroll point of the first one, if it is html or body + // return the scroll point of the real top level element. + return originalScrollTop.apply($().getScrollingElement(), arguments); + } + return originalScrollTop.apply(this, arguments); +}; +const originalAnimate = $.fn.animate; +$.fn.animate = function (properties, ...rest) { + const props = Object.assign({}, properties); + if ('scrollTop' in props && this.filter('html, body').length) { + // The caller wants to scroll a set of elements including html and/or + // body to a specific point -> do that but make sure to add the real + // top level element to that set of elements if any different is found. + originalAnimate.call(this.not('html, body').add($().getScrollingElement()), {'scrollTop': props['scrollTop']}, ...rest); + delete props['scrollTop']; + } + if (!Object.keys(props).length) { + return this; + } + return originalAnimate.call(this, props, ...rest); +}; +}); diff --git a/addons/web/static/src/js/libs/pdfjs.js b/addons/web/static/src/js/libs/pdfjs.js new file mode 100644 index 00000000..92c1e560 --- /dev/null +++ b/addons/web/static/src/js/libs/pdfjs.js @@ -0,0 +1,20 @@ +/* +* There is no changes to pdf.js in this file, but only a note about a change that has been done in it. +* +* In the module account_invoice_extract, the the code need to react to the 'pagerendered' event triggered by +* pdf.js. However in recent version of pdf.js, event are not visible outside of the library, except if the +* 'eventBusDispatchToDOM' has been set to true. +* +* We tried to set this option from outside of the library but without success, as our pdf viewer is in an iframe. +* There is no state of the iframe in which we can add an event listener to set the option. +* pdf.js has an event used to signal when we can set settings, called 'webviewerloaded'. +* This event is triggered in an EventListener attached to the 'DOMContentLoaded' event. +* So, to list options we had, we could: +* a) add an eventListener to the iframe document or window to react to 'webviewerloaded'. This doesn't work as +* document and windows are not the definitive ones and won't catche the event later. +* b) add an eventListener to the iframe to react to 'DOMContentLoaded', which doens't work too as our listener will be called +* after the pdf.js one. +* +* Finally the option was choosed to modify the default value of this option directly in pdf.js as no hook worked in the +* 'account_invoice_extract' module. +*/ diff --git a/addons/web/static/src/js/libs/popper.js b/addons/web/static/src/js/libs/popper.js new file mode 100644 index 00000000..e94cf40c --- /dev/null +++ b/addons/web/static/src/js/libs/popper.js @@ -0,0 +1,2 @@ +/** @odoo-module **/ +Popper.Defaults.modifiers.preventOverflow.priority = ['right', 'left', 'bottom', 'top']; diff --git a/addons/web/static/src/js/libs/underscore.js b/addons/web/static/src/js/libs/underscore.js new file mode 100644 index 00000000..083ee22f --- /dev/null +++ b/addons/web/static/src/js/libs/underscore.js @@ -0,0 +1,52 @@ +/** + * The _throttle in underscore has the feature to cancel the throttled function + * only starting version 1.9.0 + * @todo remove this in master and update underscorejs to 1.9.1 + */ + +// Returns a function, that, when invoked, will only be triggered at most once +// during a given window of time. Normally, the throttled function will run +// as much as it can, without ever going more than once per `wait` duration; +// but if you'd like to disable the execution on the leading edge, pass +// `{leading: false}`. To disable execution on the trailing edge, ditto. + +_.cancellableThrottleRemoveMeSoon = function (func, wait, options) { + var timeout, context, args, result; + var previous = 0; + if (!options) options = {}; + + var later = function () { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + var throttled = function () { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function () { + clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; +}; diff --git a/addons/web/static/src/js/libs/zoomodoo.js b/addons/web/static/src/js/libs/zoomodoo.js new file mode 100644 index 00000000..07da384c --- /dev/null +++ b/addons/web/static/src/js/libs/zoomodoo.js @@ -0,0 +1,353 @@ +odoo.define('web.zoomodoo', function (require) { +'use strict'; + +/** + This code has been more that widely inspired by easyZoom library. + + Copyright 2013 Matt Hinchliffe + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. +**/ + +var dw, dh, rw, rh, lx, ly; + +var defaults = { + + // Attribute to retrieve the zoom image URL from. + linkTag: 'a', + linkAttribute: 'data-zoom-image', + + // event to trigger zoom + event: 'click', //or mouseenter + + // Timer before trigger zoom + timer: 0, + + // Prevent clicks on the zoom image link. + preventClicks: true, + + // disable on mobile + disabledOnMobile: true, + + // Callback function to execute before the flyout is displayed. + beforeShow: $.noop, + + // Callback function to execute before the flyout is removed. + beforeHide: $.noop, + + // Callback function to execute when the flyout is displayed. + onShow: $.noop, + + // Callback function to execute when the flyout is removed. + onHide: $.noop, + + // Callback function to execute when the cursor is moved while over the image. + onMove: $.noop, + + // Callback function to execute when the flyout is attached to the target. + beforeAttach: $.noop + +}; + +/** + * ZoomOdoo + * @constructor + * @param {Object} target + * @param {Object} options (Optional) + */ +function ZoomOdoo(target, options) { + this.$target = $(target); + this.opts = $.extend({}, defaults, options, this.$target.data()); + + if (this.isOpen === undefined) { + this._init(); + } +} + +/** + * Init + * @private + */ +ZoomOdoo.prototype._init = function () { + if (window.outerWidth > 467 || !this.opts.disabledOnMobile) { + this.$link = this.$target.find(this.opts.linkTag).length && this.$target.find(this.opts.linkTag) || this.$target; + this.$image = this.$target.find('img').length && this.$target.find('img') || this.$target; + this.$flyout = $('<div class="zoomodoo-flyout" />'); + + var $attach = this.$target; + if (this.opts.attach !== undefined && this.$target.closest(this.opts.attach).length) { + $attach = this.$target.closest(this.opts.attach); + } + $attach.parent().on('mousemove.zoomodoo touchmove.zoomodoo', $.proxy(this._onMove, this)); + $attach.parent().on('mouseleave.zoomodoo touchend.zoomodoo', $.proxy(this._onLeave, this)); + this.$target.on(this.opts.event + '.zoomodoo touchstart.zoomodoo', $.proxy(this._onEnter, this)); + + if (this.opts.preventClicks) { + this.$target.on('click.zoomodoo', function (e) { e.preventDefault(); }); + } else { + var self = this; + this.$target.on('click.zoomodoo', function () { self.hide(); self.$target.unbind(); }); + } + } +}; + +/** + * Show + * @param {MouseEvent|TouchEvent} e + * @param {Boolean} testMouseOver (Optional) + */ +ZoomOdoo.prototype.show = function (e, testMouseOver) { + var w1, h1, w2, h2; + var self = this; + + if (this.opts.beforeShow.call(this) === false) return; + + if (!this.isReady) { + return this._loadImage(this.$link.attr(this.opts.linkAttribute), function () { + if (self.isMouseOver || !testMouseOver) { + self.show(e); + } + }); + } + + var $attach = this.$target; + if (this.opts.attach !== undefined && this.$target.closest(this.opts.attach).length) { + $attach = this.$target.closest(this.opts.attach); + } + + // Prevents having multiple zoom flyouts + $attach.parent().find('.zoomodoo-flyout').remove(); + this.$flyout.removeAttr('style'); + $attach.parent().append(this.$flyout); + + if (this.opts.attachToTarget) { + this.opts.beforeAttach.call(this); + + // Be sure that the flyout is at top 0, left 0 to ensure correct computation + // e.g. employees kanban on dashboard + this.$flyout.css('position', 'fixed'); + var flyoutOffset = this.$flyout.offset(); + if (flyoutOffset.left > 0) { + var flyoutLeft = parseFloat(this.$flyout.css('left').replace('px','')); + this.$flyout.css('left', flyoutLeft - flyoutOffset.left + 'px'); + } + if (flyoutOffset.top > 0) { + var flyoutTop = parseFloat(this.$flyout.css('top').replace('px','')); + this.$flyout.css('top', flyoutTop - flyoutOffset.top + 'px'); + } + + if(this.$zoom.height() < this.$flyout.height()) { + this.$flyout.css('height', this.$zoom.height() + 'px'); + } + if(this.$zoom.width() < this.$flyout.width()) { + this.$flyout.css('width', this.$zoom.width() + 'px'); + } + + var offset = this.$target.offset(); + var left = offset.left - this.$flyout.width(); + var top = offset.top; + + // Position the zoom on the right side of the target + // if there's not enough room on the left + if(left < 0) { + if(offset.left < ($(document).width() / 2)) { + left = offset.left + this.$target.width(); + } else { + left = 0; + } + } + + // Prevents the flyout to overflow + if(left + this.$flyout.width() > $(document).width()) { + this.$flyout.css('width', $(document).width() - left + 'px'); + } else if(left === 0) { // Limit the max width if displayed on the left + this.$flyout.css('width', offset.left + 'px'); + } + + // Prevents the zoom to be displayed outside the current viewport + if((top + this.$flyout.height()) > $(document).height()) { + top = $(document).height() - this.$flyout.height(); + } + + this.$flyout.css('transform', 'translate3d(' + left + 'px, ' + top + 'px, 0px)'); + } else { + // Computing flyout max-width depending to the available space on the right to avoid overflow-x issues + // e.g. width too high so a right zoomed element is not visible (need to scroll on x axis) + var rightAvailableSpace = document.body.clientWidth - this.$flyout[0].getBoundingClientRect().left; + this.$flyout.css('max-width', rightAvailableSpace); + } + + w1 = this.$target[0].offsetWidth; + h1 = this.$target[0].offsetHeight; + + w2 = this.$flyout.width(); + h2 = this.$flyout.height(); + + dw = this.$zoom.width() - w2; + dh = this.$zoom.height() - h2; + + // For the case where the zoom image is actually smaller than + // the flyout. + if (dw < 0) dw = 0; + if (dh < 0) dh = 0; + + rw = dw / w1; + rh = dh / h1; + + this.isOpen = true; + + this.opts.onShow.call(this); + + if (e) { + this._move(e); + } +}; + +/** + * On enter + * @private + * @param {Event} e + */ +ZoomOdoo.prototype._onEnter = function (e) { + var self = this; + var touches = e.originalEvent.touches; + e.preventDefault(); + this.isMouseOver = true; + + setTimeout(function () { + if (self.isMouseOver && (!touches || touches.length === 1)) { + self.show(e, true); + } + }, this.opts.timer); + +}; + +/** + * On move + * @private + * @param {Event} e + */ +ZoomOdoo.prototype._onMove = function (e) { + if (!this.isOpen) return; + + e.preventDefault(); + this._move(e); +}; + +/** + * On leave + * @private + */ +ZoomOdoo.prototype._onLeave = function () { + this.isMouseOver = false; + if (this.isOpen) { + this.hide(); + } +}; + +/** + * On load + * @private + * @param {Event} e + */ +ZoomOdoo.prototype._onLoad = function (e) { + // IE may fire a load event even on error so test the image dimensions + if (!e.currentTarget.width) return; + + this.isReady = true; + + this.$flyout.html(this.$zoom); + + if (e.data.call) { + e.data(); + } +}; + +/** + * Load image + * @private + * @param {String} href + * @param {Function} callback + */ +ZoomOdoo.prototype._loadImage = function (href, callback) { + var zoom = new Image(); + + this.$zoom = $(zoom).on('load', callback, $.proxy(this._onLoad, this)); + + zoom.style.position = 'absolute'; + zoom.src = href; +}; + +/** + * Move + * @private + * @param {Event} e + */ +ZoomOdoo.prototype._move = function (e) { + if (e.type.indexOf('touch') === 0) { + var touchlist = e.touches || e.originalEvent.touches; + lx = touchlist[0].pageX; + ly = touchlist[0].pageY; + } else { + lx = e.pageX || lx; + ly = e.pageY || ly; + } + + var offset = this.$target.offset(); + var pt = ly - offset.top; + var pl = lx - offset.left; + var xt = Math.ceil(pt * rh); + var xl = Math.ceil(pl * rw); + + // Close if outside + if (!this.opts.attachToTarget && (xl < 0 || xt < 0 || xl > dw || xt > dh || lx > (offset.left + this.$target.outerWidth()))) { + this.hide(); + } else { + var top = xt * -1; + var left = xl * -1; + + this.$zoom.css({ + top: top, + left: left + }); + + this.opts.onMove.call(this, top, left); + } + +}; + +/** + * Hide + */ +ZoomOdoo.prototype.hide = function () { + if (!this.isOpen) return; + if (this.opts.beforeHide.call(this) === false) return; + + this.$flyout.detach(); + this.isOpen = false; + + this.opts.onHide.call(this); +}; + +// jQuery plugin wrapper +$.fn.zoomOdoo = function (options) { + return this.each(function () { + var api = $.data(this, 'zoomOdoo'); + + if (!api) { + $.data(this, 'zoomOdoo', new ZoomOdoo(this, options)); + } else if (api.isOpen === undefined) { + api._init(); + } + }); +}; +}); |
