diff --git a/README.md b/README.md index f1575d0..a63df4b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ var editor = EditorJS({ hyperlink: { Save: 'Salvar', 'Select target': 'Seleziona destinazione', - 'Select rel': 'Wählen rel' + 'Select rel': 'Wählen rel', + 'The URL is not valid.': 'The URL is not valid.' } } } @@ -83,7 +84,8 @@ var editor = EditorJS({ | availableTargets | `string[]` | Available link targets, defaults to all targets.
If empty array is provided, the control will be hidden and the default value applied. | | availableRels | `string[]` | Available link rels, defaults to all rels.
If empty array is provided, the control will be hidden and the default value applied. | | validate | `boolean` | Defines if an URL should be validated on saving | +| configure | `function` | Function that accepts URL and returns attributes object with optional fields: `{href:"https://...",target:"_blank",rels:{nofollow: true}}` | ## License -[MIT](https://tamit.info) \ No newline at end of file +[MIT](https://tamit.info) diff --git a/src/Hyperlink.css b/src/Hyperlink.css index 46ab9c6..0c49aae 100644 --- a/src/Hyperlink.css +++ b/src/Hyperlink.css @@ -5,7 +5,7 @@ margin: 0; font-size: 13px; padding: 10px; - width: 100%; + width: 300px; -webkit-box-sizing: border-box; box-sizing: border-box; display: none; @@ -18,8 +18,7 @@ } .ce-inline-tool-hyperlink--input, -.ce-inline-tool-hyperlink--select-target, -.ce-inline-tool-hyperlink--select-rel { +.ce-inline-tool-hyperlink--select-target { border: 1px solid rgba(201,201,204,.48); -webkit-box-shadow: inset 0 1px 2px 0 rgba(35,44,72,.06); box-shadow: inset 0 1px 2px 0 rgba(35,44,72,.06); @@ -32,16 +31,28 @@ box-sizing: border-box; } -.ce-inline-tool-hyperlink--select-target, -.ce-inline-tool-hyperlink--select-rel { - width: 48%; +.ce-inline-tool-hyperlink--select-target { + width: 100%; display: inline-block; } .ce-inline-tool-hyperlink--select-target { margin-right: 2%; } + .ce-inline-tool-hyperlink--select-rel { - margin-left: 2%; + display: flex; + flex-wrap: wrap; + margin-bottom: 10px; +} + +.ce-inline-tool-hyperlink--checkbox-label { + display: flex; + flex: 50%; + padding: 3px 10px; + align-items: center; +} +.ce-inline-tool-hyperlink--checkbox-input { + margin-right: 5px; } .ce-inline-tool-hyperlink--button { diff --git a/src/Hyperlink.js b/src/Hyperlink.js index a8d4142..9f1342e 100644 --- a/src/Hyperlink.js +++ b/src/Hyperlink.js @@ -1,5 +1,4 @@ -import SelectionUtils from "./SelectionUtils"; -import css from './Hyperlink.css'; +import SelectionUtils from './SelectionUtils'; export default class Hyperlink { @@ -24,6 +23,8 @@ export default class Hyperlink { input: 'ce-inline-tool-hyperlink--input', selectTarget: 'ce-inline-tool-hyperlink--select-target', selectRel: 'ce-inline-tool-hyperlink--select-rel', + checkboxLabel: 'ce-inline-tool-hyperlink--checkbox-label', + checkboxInput: 'ce-inline-tool-hyperlink--checkbox-input', buttonSave: 'ce-inline-tool-hyperlink--button', }; @@ -79,19 +80,18 @@ export default class Hyperlink { this.nodes.input = document.createElement('input'); this.nodes.input.placeholder = 'https://...'; this.nodes.input.classList.add(this.CSS.input); - - let i; + this.nodes.input.addEventListener('blur', this.onBlur.bind(this)); // Target this.nodes.selectTarget = document.createElement('select'); this.nodes.selectTarget.classList.add(this.CSS.selectTarget); this.addOption(this.nodes.selectTarget, this.i18n.t('Select target'), ''); - for (i=0; i 0) { + if (!!this.targetAttributes && this.targetAttributes.length > 0) { this.nodes.wrapper.appendChild(this.nodes.selectTarget); } - if(!!this.relAttributes && this.relAttributes.length > 0) { + if (!!this.relAttributes && this.relAttributes.length > 0) { this.nodes.wrapper.appendChild(this.nodes.selectRel); } @@ -183,7 +178,7 @@ export default class Hyperlink { }; } - checkState(selection=null) { + checkState(selection = null) { const anchorTag = this.selection.findParentTag('A'); if (anchorTag) { this.nodes.button.classList.add(this.CSS.buttonUnlink); @@ -191,10 +186,12 @@ export default class Hyperlink { this.openActions(); const hrefAttr = anchorTag.getAttribute('href'); const targetAttr = anchorTag.getAttribute('target'); - const relAttr = anchorTag.getAttribute('rel'); + const relAttr = (anchorTag.getAttribute('rel') || '').split(' '); this.nodes.input.value = !!hrefAttr ? hrefAttr : ''; this.nodes.selectTarget.value = !!targetAttr ? targetAttr : ''; - this.nodes.selectRel.value = !!relAttr ? relAttr : ''; + for (const checkbox of this.nodes.selectRel.getElementsByClassName(this.CSS.checkboxInput)) { + checkbox.checked = relAttr.indexOf(checkbox.dataset.rel) !== -1; + } this.selection.save(); } else { this.nodes.button.classList.remove(this.CSS.buttonUnlink); @@ -223,6 +220,49 @@ export default class Hyperlink { this.inputOpened = true; } + onBlur() { + const value = this.nodes.input.value || ''; + + if (!!this.config.validate && !!this.config.validate === true && !this.validateURL(value)) { + this.tooltip.show(this.nodes.input, this.i18n.t('The URL is not valid.'), { + placement: 'top', + }); + setTimeout(() => { + this.tooltip.hide(); + }, 1000); + return; + } + + if (typeof this.config.configure === 'function') { + Promise.resolve(this.config.configure(value)).then(data => { + if (data.href) { + this.nodes.input.value = data.href; + } + + if (typeof data.target === 'string') { + this.nodes.selectTarget.value = data.target; + } + + if (typeof data.rels === 'object') { + const rels = {}; + if (data.rels instanceof Array) { + for (const rel of data.rels) { + rels[rel] = true; + } + } else { + Object.assign(rels, data.rels); + } + + for (const checkbox of this.nodes.selectRel.getElementsByClassName(this.CSS.checkboxInput)) { + if (checkbox.dataset.rel in rels) { + checkbox.checked = rels[checkbox.dataset.rel]; + } + } + } + }); + } + } + closeActions(clearSavedSelection = true) { if (this.selection.isFakeBackgroundEnabled) { const currentSelection = new SelectionUtils(); @@ -234,7 +274,9 @@ export default class Hyperlink { this.nodes.wrapper.classList.remove(this.CSS.wrapperShowed); this.nodes.input.value = ''; this.nodes.selectTarget.value = ''; - this.nodes.selectRel.value = ''; + for (const checkbox of this.nodes.selectRel.getElementsByClassName(this.CSS.checkboxInput)) { + checkbox.checked = false; + } if (clearSavedSelection) { this.selection.clearSaved(); @@ -249,17 +291,23 @@ export default class Hyperlink { let value = this.nodes.input.value || ''; let target = this.nodes.selectTarget.value || ''; - let rel = this.nodes.selectRel.value || ''; + const rels = []; + for (const checkbox of this.nodes.selectRel.getElementsByClassName(this.CSS.checkboxInput)) { + if (checkbox.checked) { + rels.push(checkbox.dataset.rel); + } + } if (!value.trim()) { this.selection.restore(); this.unlink(); event.preventDefault(); this.closeActions(); + return; } if (!!this.config.validate && !!this.config.validate === true && !this.validateURL(value)) { - this.tooltip.show(this.nodes.input, 'The URL is not valid.', { + this.tooltip.show(this.nodes.input, this.i18n.t('The URL is not valid.'), { placement: 'top', }); setTimeout(() => { @@ -273,19 +321,19 @@ export default class Hyperlink { this.selection.restore(); this.selection.removeFakeBackground(); - this.insertLink(value, target, rel); + this.insertLink(value, target, rels.join(' ')); this.selection.collapseToEnd(); this.inlineToolbar.close(); } validateURL(str) { - const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string - '(\\#[-a-z\\d_]*)?$','i'); // fragment locator + const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator return !!pattern.test(str); } @@ -311,23 +359,23 @@ export default class Hyperlink { return link; } - insertLink(link, target='', rel='') { + insertLink(link, target = '', rel = '') { let anchorTag = this.selection.findParentTag('A'); if (anchorTag) { this.selection.expandToTag(anchorTag); - }else{ + } else { document.execCommand(this.commandLink, false, link); anchorTag = this.selection.findParentTag('A'); } - if(anchorTag) { - if(!!target) { + if (anchorTag) { + if (!!target) { anchorTag['target'] = target; - }else{ + } else { anchorTag.removeAttribute('target'); } - if(!!rel) { + if (!!rel) { anchorTag['rel'] = rel; - }else{ + } else { anchorTag.removeAttribute('rel'); } } @@ -346,10 +394,26 @@ export default class Hyperlink { return icon; } - addOption(element, text, value=null) { + addOption(element, text, value = null) { let option = document.createElement('option'); option.text = text; option.value = value; element.add(option); } + + addCheckbox(element, text, value, checked = false) { + let input = document.createElement('input'); + input.type = 'checkbox'; + input.classList.add(this.CSS.checkboxInput); + input.dataset.rel = value; + input.checked = checked; + + const label = document.createElement('label'); + label.classList.add(this.CSS.checkboxLabel); + + label.appendChild(input); + label.insertAdjacentText('beforeend', text); + + element.appendChild(label); + } }