diff --git a/.eslintrc b/.eslintrc index b5b67a58..6aef4137 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,6 +5,7 @@ "globals": { "fetch": true, "ImageConfig": true, - "ImageToolData": true + "ImageToolData": true, + "quote-props": ["error", "consistent"] } } diff --git a/.gitignore b/.gitignore index 641d5eb0..80f295b2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ node_modules/* npm-debug.log .idea/ .DS_Store -dist diff --git a/README.md b/README.md index cdba3830..c8c662b9 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Image Tool supports these configuration parameters: | buttonContent | `string` | Allows to override HTML content of «Select file» button | | uploader | `{{uploadByFile: function, uploadByUrl: function}}` | Optional custom uploading methods. See details below. | | actions | `array` | Array with custom actions to show in the tool's settings menu. See details below. | +| attributes | `object` | Object with attribute names to add to block html output such as lazy loading tag | Note that if you don't implement your custom uploader methods, the `endpoints` param is required. @@ -187,6 +188,12 @@ The response of your uploader **should** cover the following format: "file": { "url" : "https://www.tesla.com/tesla_theme/assets/img/_vehicle_redesign/roadster_and_semi/roadster/hero.jpg", // ... and any additional fields you want to store, such as width, height, color, extension, etc + "attributes" : { + "srcset": "clock-demo-200px.png 200w, clock-demo-400px.png 400w", + "width" : "400", + "height" : "400", + // ... and any attributes you would like the image / video element tag to have such as adding width, height, etc + } } } ``` @@ -289,6 +296,12 @@ var editor = EditorJS({ }) } } + /** + * Ability to added custom attribute to block output such as lazy loading tag + */ + attributes: { + srcset: 'clock-demo-200px.png 200w, clock-demo-400px.png 400w' + } } } } diff --git a/dist/bundle.js b/dist/bundle.js new file mode 100644 index 00000000..b0dfee5e --- /dev/null +++ b/dist/bundle.js @@ -0,0 +1,41 @@ +/*! + * Image tool + * + * @version 2.8.1 + * + * @package https://github.com/editor-js/image + * @licence MIT + * @author CodeX + */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ImageTool=t():e.ImageTool=t()}(window,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=10)}([function(e,t){function n(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};if(e.url&&"string"!=typeof e.url)throw new Error("Url must be a string");if(e.url=e.url||"",e.method&&"string"!=typeof e.method)throw new Error("`method` must be a string or null");if(e.method=e.method?e.method.toUpperCase():"GET",e.headers&&"object"!==r(e.headers))throw new Error("`headers` must be an object or null");if(e.headers=e.headers||{},e.type&&("string"!=typeof e.type||!Object.values(o).includes(e.type)))throw new Error("`type` must be taken from module's «contentType» library");if(e.progress&&"function"!=typeof e.progress)throw new Error("`progress` must be a function or null");if(e.progress=e.progress||function(e){},e.beforeSend=e.beforeSend||function(e){},e.ratio&&"number"!=typeof e.ratio)throw new Error("`ratio` must be a number");if(e.ratio<0||e.ratio>100)throw new Error("`ratio` must be in a 0-100 interval");if(e.ratio=e.ratio||90,e.accept&&"string"!=typeof e.accept)throw new Error("`accept` must be a string with a list of allowed mime-types");if(e.accept=e.accept||"*/*",e.multiple&&"boolean"!=typeof e.multiple)throw new Error("`multiple` must be a true or false");if(e.multiple=e.multiple||!1,e.fieldName&&"string"!=typeof e.fieldName)throw new Error("`fieldName` must be a string");return e.fieldName=e.fieldName||"files",e},c=function(e){switch(e.method){case"GET":var t=s(e.data,o.URLENCODED);delete e.data,e.url=/\?/.test(e.url)?e.url+"&"+t:e.url+"?"+t;break;case"POST":case"PUT":case"DELETE":case"UPDATE":var n=function(){return(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).type||o.JSON}(e);(d.isFormData(e.data)||d.isFormElement(e.data))&&(n=o.FORM),e.data=s(e.data,n),n!==f.contentType.FORM&&(e.headers["content-type"]=n)}return e},s=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};switch(arguments.length>1?arguments[1]:void 0){case o.URLENCODED:return d.urlEncode(e);case o.JSON:return d.jsonEncode(e);case o.FORM:return d.formEncode(e);default:return e}},l=function(e){return e>=200&&e<300},{contentType:o={URLENCODED:"application/x-www-form-urlencoded; charset=utf-8",FORM:"multipart/form-data",JSON:"application/json; charset=utf-8"},request:i,get:function(e){return e.method="GET",i(e)},post:a,transport:function(e){return e=u(e),d.selectFiles(e).then((function(t){for(var n=new FormData,r=0;r=0&&(e._idleTimeoutId=setTimeout((function(){e._onTimeout&&e._onTimeout()}),t))},n(6),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(0))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var r,o,i,a,u,c=1,s={},l=!1,d=e.document,f=Object.getPrototypeOf&&Object.getPrototypeOf(e);f=f&&f.setTimeout?f:e,"[object process]"==={}.toString.call(e.process)?r=function(e){t.nextTick((function(){h(e)}))}:function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?(a="setImmediate$"+Math.random()+"$",u=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(a)&&h(+t.data.slice(a.length))},e.addEventListener?e.addEventListener("message",u,!1):e.attachEvent("onmessage",u),r=function(t){e.postMessage(a+t,"*")}):e.MessageChannel?((i=new MessageChannel).port1.onmessage=function(e){h(e.data)},r=function(e){i.port2.postMessage(e)}):d&&"onreadystatechange"in d.createElement("script")?(o=d.documentElement,r=function(e){var t=d.createElement("script");t.onreadystatechange=function(){h(e),t.onreadystatechange=null,o.removeChild(t),t=null},o.appendChild(t)}):r=function(e){setTimeout(h,0,e)},f.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n1)for(var n=1;n HTMLElement")}},{key:"isObject",value:function(e){return"[object Object]"===Object.prototype.toString.call(e)}},{key:"isFormData",value:function(e){return e instanceof FormData}},{key:"isFormElement",value:function(e){return e instanceof HTMLFormElement}},{key:"selectFiles",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return new Promise((function(t,n){var r=document.createElement("INPUT");r.type="file",e.multiple&&r.setAttribute("multiple","multiple"),e.accept&&r.setAttribute("accept",e.accept),r.style.display="none",document.body.appendChild(r),r.addEventListener("change",(function(e){var n=e.target.files;t(n),document.body.removeChild(r)}),!1),r.click()}))}},{key:"parseHeaders",value:function(e){var t=e.trim().split(/[\r\n]+/),n={};return t.forEach((function(e){var t=e.split(": "),r=t.shift(),o=t.join(": ");r&&(n[r]=o)})),n}}])&&r(t,n),e}()},function(e,t){var n=function(e){return encodeURIComponent(e).replace(/[!'()*]/g,escape).replace(/%20/g,"+")},r=function(e,t,o,i){return t=t||null,o=o||"&",i=i||null,e?function(e){for(var t=new Array,n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return r("end");if(i.tryLoc<=this.prev){var u=n.call(i,"catchLoc"),c=n.call(i,"finallyLoc");if(u&&c){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--t){var n=this.tryEntries[t];if(n.finallyLoc===e)return this.complete(n.completion,n.afterLoc),k(n),s}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var n=this.tryEntries[t];if(n.tryLoc===e){var r=n.completion;if("throw"===r.type){var o=r.arg;k(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:x(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=void 0),s}},e}(e.exports);try{regeneratorRuntime=r}catch(e){Function("r","regeneratorRuntime = r")(r)}},function(e,t,n){var r=n(13),o=n(14);"string"==typeof(o=o.__esModule?o.default:o)&&(o=[[e.i,o,""]]);var i={insert:"head",singleton:!1},a=(r(o,i),o.locals?o.locals:{});e.exports=a},function(e,t,n){"use strict";var r,o=function(){return void 0===r&&(r=Boolean(window&&document&&document.all&&!window.atob)),r},i=function(){var e={};return function(t){if(void 0===e[t]){var n=document.querySelector(t);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}e[t]=n}return e[t]}}(),a=[];function u(e){for(var t=-1,n=0;n1&&void 0!==arguments[1]?arguments[1]:null,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=document.createElement(e);Array.isArray(n)?(t=o.classList).add.apply(t,m()(n)):n&&o.classList.add(n);for(var i in r)o[i]=r[i];return o}function y(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}var v=function(){function e(t){var n=t.api,r=t.config,o=t.onSelectFile,i=t.readOnly;c()(this,e),this.api=n,this.config=r,this.onSelectFile=o,this.readOnly=i,this.nodes={wrapper:g("div",[this.CSS.baseClass,this.CSS.wrapper]),imageContainer:g("div",[this.CSS.imageContainer]),fileButton:this.createFileButton(),imageEl:void 0,imagePreloader:g("div",this.CSS.imagePreloader),caption:g("div",[this.CSS.input,this.CSS.caption],{contentEditable:!this.readOnly})},this.nodes.caption.dataset.placeholder=this.config.captionPlaceholder,this.nodes.imageContainer.appendChild(this.nodes.imagePreloader),this.nodes.wrapper.appendChild(this.nodes.imageContainer),this.nodes.wrapper.appendChild(this.nodes.caption),this.nodes.wrapper.appendChild(this.nodes.fileButton)}return l()(e,[{key:"render",value:function(t){return t.file&&0!==Object.keys(t.file).length?this.toggleStatus(e.status.UPLOADING):this.toggleStatus(e.status.EMPTY),this.nodes.wrapper}},{key:"createFileButton",value:function(){var e=this,t=g("div",[this.CSS.button]);return t.innerHTML=this.config.buttonContent||"".concat(p," ").concat(this.api.i18n.t("Select an Image")),t.addEventListener("click",(function(){e.onSelectFile()})),t}},{key:"showPreloader",value:function(t){this.nodes.imagePreloader.style.backgroundImage="url(".concat(t,")"),this.toggleStatus(e.status.UPLOADING)}},{key:"hidePreloader",value:function(){this.nodes.imagePreloader.style.backgroundImage="",this.toggleStatus(e.status.EMPTY)}},{key:"fillImage",value:function(t){var n=this,r=t.url,o=t.attributes||{},i=this.config.attributes||{},a=/\.mp4$/.test(r)?"VIDEO":"IMG",u=function(e){for(var t=1;t',title:"With border",toggle:!0},{name:"stretched",icon:'',title:"Stretch image",toggle:!0},{name:"withBackground",icon:'',title:"With background",toggle:!0}]}}]),l()(e,[{key:"render",value:function(){return this.ui.render(this.data)}},{key:"validate",value:function(e){return e.file&&e.file.url}},{key:"save",value:function(){var e=this.ui.nodes.caption;return this._data.caption=e.innerHTML,this.data}},{key:"renderSettings",value:function(){var t=this;return e.tunes.concat(this.config.actions).map((function(e){return{icon:e.icon,label:t.api.i18n.t(e.title),name:e.name,toggle:e.toggle,isActive:t.data[e.name],onActivate:function(){"function"!=typeof e.action?t.tuneToggled(e.name):e.action(e.name)}}}))}},{key:"appendCallback",value:function(){this.ui.nodes.fileButton.click()}},{key:"onPaste",value:(t=a()(o.a.mark((function e(t){var n,r,i,a,u;return o.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:e.t0=t.type,e.next="tag"===e.t0?3:"pattern"===e.t0?15:"file"===e.t0?18:21;break;case 3:if(n=t.detail.data,!/^blob:/.test(n.src)){e.next=13;break}return e.next=7,fetch(n.src);case 7:return r=e.sent,e.next=10,r.blob();case 10:return i=e.sent,this.uploadFile(i),e.abrupt("break",21);case 13:return this.uploadUrl(n.src),e.abrupt("break",21);case 15:return a=t.detail.data,this.uploadUrl(a),e.abrupt("break",21);case 18:return u=t.detail.file,this.uploadFile(u),e.abrupt("break",21);case 21:case"end":return e.stop()}}),e,this)}))),function(e){return t.apply(this,arguments)})},{key:"onUpload",value:function(e){e.success&&e.file?this.image=e.file:this.uploadingFailed("incorrect response: "+JSON.stringify(e))}},{key:"uploadingFailed",value:function(e){console.log("Image Tool: uploading failed because of",e),this.api.notifier.show({message:this.api.i18n.t("Couldn’t upload image. Please try another."),style:"error"}),this.ui.hidePreloader()}},{key:"tuneToggled",value:function(e){this.setTune(e,!this._data[e])}},{key:"setTune",value:function(e,t){var n=this;this._data[e]=t,this.ui.applyTune(e,t),"stretched"===e&&Promise.resolve().then((function(){var e=n.api.blocks.getCurrentBlockIndex();n.api.blocks.stretchBlock(e,t)})).catch((function(e){console.error(e)}))}},{key:"uploadFile",value:function(e){var t=this;this.uploader.uploadByFile(e,{onPreview:function(e){t.ui.showPreloader(e)}})}},{key:"uploadUrl",value:function(e){this.ui.showPreloader(e),this.uploader.uploadByUrl(e)}},{key:"data",set:function(t){var n=this;this.image=t.file,this._data.caption=t.caption||"",this.ui.fillCaption(this._data.caption),e.tunes.forEach((function(e){var r=e.name,o=void 0!==t[r]&&(!0===t[r]||"true"===t[r]);n.setTune(r,o)}))},get:function(){return this._data}},{key:"image",set:function(e){this._data.file=e||{},e&&e.url&&this.ui.fillImage(e)}}],[{key:"pasteConfig",get:function(){return{tags:[{img:{src:!0}}],patterns:{image:/https?:\/\/\S+\.(gif|jpe?g|tiff|png|svg|webp)(\?[a-z0-9=]*)?$/i},files:{mimeTypes:["image/*"]}}}}]),e}(); +/** + * Image Tool for the Editor.js + * + * @author CodeX + * @license MIT + * @see {@link https://github.com/editor-js/image} + * + * To developers. + * To simplify Tool structure, we split it to 4 parts: + * 1) index.js — main Tool's interface, public API and methods for working with data + * 2) uploader.js — module that has methods for sending files via AJAX: from device, by URL or File pasting + * 3) ui.js — module for UI manipulations: render, showing preloader, etc + * 4) tunes.js — working with Block Tunes: render buttons, handle clicks + * + * For debug purposes there is a testing server + * that can save uploaded files and return a Response {@link UploadResponseFormat} + * + * $ node dev/server.js + * + * It will expose 8008 port, so you can pass http://localhost:8008 with the Tools config: + * + * image: { + * class: ImageTool, + * config: { + * endpoints: { + * byFile: 'http://localhost:8008/uploadFile', + * byUrl: 'http://localhost:8008/fetchUrl', + * } + * }, + * }, + */}]).default})); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 2bf61c42..e8c56831 100644 --- a/src/index.js +++ b/src/index.js @@ -75,6 +75,18 @@ import { IconAddBorder, IconStretch, IconAddBackground, IconPicture } from '@cod * @property {string} file.url - [Required] image source URL */ export default class ImageTool { + + /** + * Prevents image attributes from being removed during sanitization phase + * + */ + + static get 'sanitize'() { + return { + 'img': true + }; + } + /** * Notify core that read-only mode is supported * @@ -150,6 +162,7 @@ export default class ImageTool { buttonContent: config.buttonContent || '', uploader: config.uploader || undefined, actions: config.actions || [], + attributes: config.attributes || {} }; /** @@ -288,7 +301,7 @@ export default class ImageTool { * Drag n drop file from into the Editor */ files: { - mimeTypes: [ 'image/*' ], + mimeTypes: ['image/*'], }, }; } @@ -381,7 +394,7 @@ export default class ImageTool { this._data.file = file || {}; if (file && file.url) { - this.ui.fillImage(file.url); + this.ui.fillImage(file); } } diff --git a/src/ui.js b/src/ui.js index 8609ef21..44fba8e3 100644 --- a/src/ui.js +++ b/src/ui.js @@ -22,7 +22,7 @@ export default class Ui { this.readOnly = readOnly; this.nodes = { wrapper: make('div', [this.CSS.baseClass, this.CSS.wrapper]), - imageContainer: make('div', [ this.CSS.imageContainer ]), + imageContainer: make('div', [this.CSS.imageContainer]), fileButton: this.createFileButton(), imageEl: undefined, imagePreloader: make('div', this.CSS.imagePreloader), @@ -109,7 +109,7 @@ export default class Ui { * @returns {Element} */ createFileButton() { - const button = make('div', [ this.CSS.button ]); + const button = make('div', [this.CSS.button]); button.innerHTML = this.config.buttonContent || `${IconPicture} ${this.api.i18n.t('Select an Image')}`; @@ -148,7 +148,10 @@ export default class Ui { * @param {string} url - image source * @returns {void} */ - fillImage(url) { + fillImage(file) { + const url = file.url; + const fileAttributes = file.attributes || {}; + const configAttributes = this.config.attributes || {}; /** * Check for a source extension to compose element correctly: video tag for mp4, img — for others */ @@ -156,6 +159,8 @@ export default class Ui { const attributes = { src: url, + ...configAttributes, + ...fileAttributes }; /**