I'm trying to write a custom handler for the link input value. In case the user inputs a link that does not have a custom protocol, I wish to prepend a http: before the input value. That's because if link value lacks http:, link is not interpreted and about:blank is shown intead. (https://github.com/quilljs/quill/issues/1268#issuecomment-272959998)
Here's what I've written (similar to the official example here):
toolbar.addHandler("link", function sanitizeLinkInput(linkValueInput) {
console.log(linkValueInput); // debugging
if (linkValueInput === "")
this.quill.format(false);
// do nothing, since it implies user has just clicked the icon
// for link, hasn't entered url yet
else if (linkValueInput == true);
// do nothing, since this implies user's already using a custom protocol
else if (/^\w+:/.test(linkValueInput));
else if (!/^https?:/.test(linkValueInput)) {
linkValueInput = "http:" + linkValueInput;
this.quill.format("link", linkValueInput);
}
});
Every time the user clicks the link icon, nothing happens and true is logged to the console. I actually wished this handler to be executed when person clicks "save" on the tooltip that's shown after pressing the link icon.
Any idea how to do this? Hints or suggestions are also appreciated.
Thanks!
congregating all the information
The snow theme itself uses the toolbar's addHandler to show a tooltip and so it is impossible to use the addHandler method to achieve what we wish to.
So, instead we can do the following:
var Link = Quill.import('formats/link');
var builtInFunc = Link.sanitize;
Link.sanitize = function customSanitizeLinkInput(linkValueInput) {
var val = linkValueInput;
// do nothing, since this implies user's already using a custom protocol
if (/^\w+:/.test(val));
else if (!/^https?:/.test(val))
val = "http:" + val;
return builtInFunc.call(this, val); // retain the built-in logic
};
this method doesn't hook onto handlers but instead modifies the built-in sanitisation logic itself. We have also retained the original behavior of the sanitisation so that doesn't modify the editor's original behavior.
Alternatively, we could actually hook onto the save button of the tooltip, using this code. But it is too long a method compared to the one above.
As far as I can tell, the handling of creating and updating links is a bit distributed in Quill's sources. The default Snow theme handles editing links to some extent: it tracks the user selection and last selected link internally. Because of this I do not think that it is possible to achieve what you want currently in Quill using only a custom handler.
You may want to open an issue to report this, the authors might be willing to add such a handler.
In the meantime I came up with a way to update the link by simply listening for events causing the edit tooltip to close. There are some complications, because a link can be edited and the theme then relies on its internal tracking to update it. However, all in all I think that this solution is not too bad. You might want to add some error checking here and there, but overall it seems to work nicely and do what you want it do to. I have created a Fiddle demonstrating this. For completeness, I have included it here as a code snippet too.
var quill = new Quill('#editor', {
modules: {
toolbar: true
},
theme: 'snow'
}),
editor = document.getElementById('editor'),
lastLinkRange = null;
/**
* Add protocol to link if it is missing. Considers the current selection in Quill.
*/
function updateLink() {
var selection = quill.getSelection(),
selectionChanged = false;
if (selection === null) {
var tooltip = quill.theme.tooltip;
if (tooltip.hasOwnProperty('linkRange')) {
// user started to edit a link
lastLinkRange = tooltip.linkRange;
return;
} else {
// user finished editing a link
var format = quill.getFormat(lastLinkRange),
link = format.link;
quill.setSelection(lastLinkRange.index, lastLinkRange.length, 'silent');
selectionChanged = true;
}
} else {
var format = quill.getFormat();
if (!format.hasOwnProperty('link')) {
return; // not a link after all
}
var link = format.link;
}
// add protocol if not there yet
if (!/^https?:/.test(link)) {
link = 'http:' + link;
quill.format('link', link);
// reset selection if we changed it
if (selectionChanged) {
if (selection === null) {
quill.setSelection(selection, 0, 'silent');
} else {
quill.setSelection(selection.index, selection.length, 'silent');
}
}
}
}
// listen for clicking 'save' button
editor.addEventListener('click', function(event) {
// only respond to clicks on link save action
if (event.target === editor.querySelector('.ql-tooltip[data-mode="link"] .ql-action')) {
updateLink();
}
});
// listen for 'enter' button to save URL
editor.addEventListener('keydown', function(event) {
// only respond to clicks on link save action
var key = (event.which || event.keyCode);
if (key === 13 && event.target === editor.querySelector('.ql-tooltip[data-mode="link"] input')) {
updateLink();
}
});
<link href="https://cdn.quilljs.com/1.1.10/quill.snow.css" rel="stylesheet" />
<script src="https://cdn.quilljs.com/1.1.10/quill.min.js"></script>
<div id="editor"></div>
Let me know if you have any questions.
The toolbar handler just calls your given function when the button in the toolbar is clicked. The value passed in depends on the status of that format in the user's selection. So if the user has highlighted just plain text, and clicked the link button, you will get false. If the user highlighted the link, you will get the value of the link, which is by default the url. This is explained with an example here: http://quilljs.com/docs/modules/toolbar/#handlers.
The snow theme uses toolbar's addHandler itself to show a tooltip and it looks like you are trying to hook into this, which is not possible at the moment.
It looks like what you are really trying to do is control the sanitization logic of a link. Sanitization exists at a lower level in Quill since there are many ways to insert a link, for example from the tooltip UI, from paste, from the different APIs, and more. So to cover them all the logic is in the link format itself. An example of custom formats of links specifically is covered in http://quilljs.com/guides/cloning-medium-with-parchment/#links. You can also just modify Quill's own sanitize method but this is not recommended as it is not documented nor covered by semver.
let Link = Quill.import('formats/link');
Link.sanitize = function(value) {
return 'customsanitizedvalue';
}
after spending half an hour
found this solution
htmlEditorModuleConfig = {
toolbar: [
['link']
],
bounds: document.body
}
Add 'bounds: document.body' in configuration
I have to do same exact thing,(validate url before sending to server) so I end up with some thing like this.
const editor = new DOMParser().parseFromString(value,
'text/html');
const body = qlEditor.getElementsByTagName('body');
const data = document.createElement('div');
data.innerHTML = body[0].innerHTML;
Array.from(data.querySelectorAll('a')).forEach((ele) => {
let href = ele.getAttribute('href');
if (!href.includes('http') && !href.includes('https')) {
href = `https://${href}`;
ele.setAttribute('href', href);
}
});
body[0].innerHTML = data.innerHTML;
Maybe this is an old question but this is the way I make it work.
First, it whitelist other custom protocols to be accepted as valid ones.
Then, we run the sanitize method that is already included on the Quill core, and based on the custom protocols list it will return the URL or about:blank.
Then, if this is a about:blank is because it did not pass the sanitization method. If we get the URL then we verify whether or not it has a protocol from the list and if not, then we append http:// and in that way we do not get relative URL or blank unless it is not being whitelisted:
https://your-site.com/www.apple.com
about:blank
Hope it helps anyone else having this same issue.
const Link = Quill.import('formats/link')
// Override the existing property on the Quill global object and add custom protocols
Link.PROTOCOL_WHITELIST = ['http', 'https', 'mailto', 'tel', 'radar', 'rdar', 'smb', 'sms']
class CustomLinkSanitizer extends Link {
static sanitize(url) {
// Run default sanitize method from Quill
const sanitizedUrl = super.sanitize(url)
// Not whitelisted URL based on protocol so, let's return `blank`
if (!sanitizedUrl || sanitizedUrl === 'about:blank') return sanitizedUrl
// Verify if the URL already have a whitelisted protocol
const hasWhitelistedProtocol = this.PROTOCOL_WHITELIST.some(function(protocol) {
return sanitizedUrl.startsWith(protocol)
})
if (hasWhitelistedProtocol) return sanitizedUrl
// if not, then append only 'http' to not to be a relative URL
return `http://${sanitizedUrl}`
}
}
Quill.register(CustomLinkSanitizer, true)
Related
I am using a service called Unbounced for my website landing pages. They wrap the CTA buttons on the landing pages, making them indirectly go to their destination.
For example, my CTA button first goes here:
https://www.axelgo.app/drs/clkn/https/go.axel.network/#/signup
...and then redirects to here:
https://go.axel.network/signup
This wouldn't be an issue, except Google's Cross-Domain Measurement works by decorating outbound clicks to pre-specified domains with an '_gl' parameter. Since this link is technically an internal one (axelgo.app) that redirects to the external domain (go.axel.network), it doesn't get decorated.
For another example, this is my expected result when clicking the CTA button:
https://go.axel.network/?_gl=1\*1x93dka\*_ga\*Njk2OTU0MTU1LjE2NjQyMjc5NjA.\*_ga_4YJDRFJTFE*MTY3Mjc2MzMwNC40NC4wLjE2NzI3NjMzMDQuMC4wLjA.signup
...but my result due to the wrapping behavior isthis: https://go.axel.network/signup
I've been playing around with some scripts I found here and here. I'll share these below. They do not work in my case though, and I believe it is because these may have been intended for form submissions and not button clicks? I'm not exactly sure as I don't know much JavaScript.
Here's the code I found of other people working through similar problems:
Example 1:
<script>
function decorateUrl(urlString) {
var ga = window[window['GoogleAnalyticsObject']];
var tracker;
if (ga && typeof ga.getAll === 'function') {
tracker = ga.getAll()[0]; // Uses the first tracker created on the page
urlString = (new window.gaplugins.Linker(tracker)).decorate(urlString);
}
return urlString;
}
function linkDecorator(){
if (window.module.lp.form.data.action === 'url') {
window.module.lp.form.data.url = decorateUrl(window.module.lp.form.data.url)}
};
if (document.querySelector('.lp-element.lp-pom-button')) {
document.querySelector('.lp-element.lp-pom-button').addEventListener('click', linkDecorator);
}
</script>
Example 2:
<script>
$('.lp-pom-button, .lp-pom-text a, .lp-pom-image a').click(function(event) {
var parentClass = $(this).parent().attr('class');
var isFormRedirect =
parentClass === 'lp-element lp-pom-form' &&
window.module.lp.form.data.confirmAction === 'url' &&
lp.jQuery('form').valid() === true;
if (isFormRedirect) {
ga(function(tracker) {
var linker = new window.gaplugins.Linker(tracker);
window.module.lp.form.data.confirmData = linker.decorate(window.module.lp.form.data.confirmData);
});
} else {
ga('linker:decorate', this);
}
});
</script>
again, these are not functional when I tried applying them. I don't think they were made for my use case.
Thanks in advance for any support.
Edit: here's a landing page: https://www.axelgo.app/drs/
I am using Quill (Rich Text) and need to find a way to checking to see if the text has changed when the page does a form submit. I am quite new to using Quill and have been looking at the events here. Using the text-change triggers everytime the text is changed (obviously) but I have other Form Input controls on the page which are checked on form submit to see if they have changed... I need my RTF boxes to do the same.
EDIT
I have managed to get the Event Firing using the example below. My problem now is that the event appears to trigger even when the editor is pre-populated on page load. I dont want to acknowledge these initial loads, only if the text has been changed by a user.
Two ways to do so:
1) listen for quill changes and if any occurred, raise a flag telling your form content has changed (flow: if you add a char, then delete it, your flag would be true even if resulting content is the same)
Using :
let changes = false
quill.on('text-change', function(delta, oldDelta, source) {
changes = true
})
2) comparing two snapshots of the document to detect front-end if changes occurred. You could either compare strings (with quill.getText()) this is the simplest, but you could miss lot of things, I would recommend to compare objects (with quill.getContents()) and use lodash or other deep equality objects method check.
Using:
const initialContents = quill.getContents()
const beforeSubmitContents = quill.getContents()
const hasChanged = _.isEqual(initialContents.ops, beforeSubmitContents.ops)
for detect if exis change only implement this function
quill.on('text-change', function(delta, oldDelta, source) {
if (source == 'api') {
console.log("An API call triggered this change.");
} else if (source == 'user') {
console.log("A user action triggered this change.");
}
});
this function detect if write or have a change on editor, detect if has change on your words or font or image...etc.. !!
In this case i use the example of official page:
page official
result :
I am using Sammy.js for my single page app. I want to create functionality similar to SO (the one when you type your question and try to leave the page and it is asking you if you are sure).
If it would not be a single page app, I would just do something like:
$(window).bind('beforeunload', function(){
return 'Are you sure you want to leave?';
});
The problem is that in single page app user do not actually leave the page, but rather changing his document.location.hash (he can leave the page by closing it). Is there a way to make something similar for a SPA, preferably with sammy.js?
We had a similar problem to solve in our Single Page Webapp at my work. We had some pages that could be dirty and, if they were, we wanted to prevent navigation away from that page until a user verifies it's okay to do so. Since we wanted to prevent navigation, we couldn't listen for the onhashchange event, which is fired after the hash is changed, not before. Therefore, we decided to override the default LocationProxy to include logic that allowed us to optionally prevent the navigation before the location was changed.
With that in mind, here is the proxy that we used:
PreventableLocationProxy = (function () {
function PreventableLocationProxy(delegateProxy, navigationValidators) {
/// <summary>This is an implementation of a Sammy Location Proxy that allows cancelling of setting a location based on the validators passed in.</summary>
/// <param name="delegateProxy" type="Sammy.DefaultLocationProxy">The Location Proxy which we will delegate all method calls to.</param>
/// <param name="navigationValidators" type="Function" parameterArray="true" mayBeNull="true">One or more validator functions that will be called whenever someone tries to change the location.</param>
this.delegateProxy = delegateProxy;
this.navigationValidators = Array.prototype.slice.call(arguments, 1);
}
PreventableLocationProxy.prototype.bind = function () {
this.delegateProxy.bind();
};
PreventableLocationProxy.prototype.unbind = function () {
this.delegateProxy.unbind();
};
PreventableLocationProxy.prototype.getLocation = function () {
return this.delegateProxy.getLocation();
};
PreventableLocationProxy.prototype.setLocation = function (new_location) {
var doNavigation = true;
_.each(this.navigationValidators, function (navValidator) {
if (_.isFunction(navValidator)) {
// I don't just want to plug the result of the validator in, it could be anything!
var okayToNavigate = navValidator(new_location);
// A validator explicitly returning false should cancel the setLocation call. All other values will
// allow navigation still.
if (okayToNavigate === false) {
doNavigation = false;
}
}
});
if (doNavigation) {
return this.delegateProxy.setLocation(new_location);
}
};
return PreventableLocationProxy;
}());
This code is pretty simple in and of itself, it is a javascript object that takes a delegate proxy, as well as one or more validator functions. If any of those validators explicitly return false, then the navigation is prevented and the location won't change. Otherwise, the navigation is allowed. In order to make this work, we had to override our anchor tags' default onclick handling to route it through Sammy.Application.setLocation. Once done, though, this cleanly allowed our application to handle the dirty page logic.
For good measure, here is our dirty page validator:
function preventNavigationIfDirty(new_location) {
/// <summary>This is an implementation of a Sammy Location Proxy that allows cancelling of setting a location based on the validators passed in.</summary>
/// <param name="new_location" type="String">The location that will be navigated to.</param>
var currentPageModels = [];
var dirtyPageModels = [];
//-----
// Get the IDs of the current virtual page(s), if any exist.
currentPageModels = _.keys(our.namespace.currentPageModels);
// Iterate through all models on the current page, looking for any that are dirty and haven't had their changes abored.
_.forEach(currentPageModels, function (currentPage) {
if (currentPage.isDirty() && currentPage.cancelled === false) {
dirtyPageModels.push(currentPage);
}
});
// I only want to show a confirmation dialog if we actually have dirty pages that haven't been cancelled.
if (dirtyPageModels.length > 0) {
// Show a dialog with the buttons okay and cancel, and listen for the okay button's onclick event.
our.namespace.confirmDirtyNavigation(true, function () {
// If the user has said they want to navigate away, then mark all dirty pages with the cancelled
// property then do the navigating again. No pages will then prevent the navigation, unlike this
// first run.
_.each(dirtyPageModels, function (dirtyPage) {
dirtyPage.cancelled = true;
});
our.namespace.sammy.setLocation(new_location);
});
// Returns false in order to explicitly cancel the navigation. We don't need to return anything in any
// other case.
return false;
}
}
Remember, this solution won't work if the user explicitly changes the location, but that wasn't a use case that we wanted to support. Hopefully this gets you closer to a solution of your own.
I’ve made a one page site. When user clicks on the menu buttons, content is loaded with ajax.
It works fine.
In order to improve SEO and to allow user to copy / past URL of different content, i use
function show_content() {
// change URL in browser bar)
window.history.pushState("", "Content", "/content.php");
// ajax
$content.load("ajax/content.php?id="+id);
}
It works fine. URL changes and the browser doesn’t reload the page
However, when user clicks on back button in browser, the url changes and the content have to be loaded.
I've done this and it works :
window.onpopstate = function(event) {
if (document.location.pathname == '/4-content.php') {
show_content_1();
}
else if (document.location.pathname == '/1-content.php') {
show_content_2();
}
else if (document.location.pathname == '/6-content.php') {
show_content_();
}
};
Do you know if there is a way to improve this code ?
What I did was passing an object literal to pushState() on page load. This way you can always go back to your first created pushState. In my case I had to push twice before I could go back. Pushing a state on page load helped me out.
HTML5 allows you to use data-attributes so for your triggers you can use those to bind HTML data.
I use a try catch because I didn't had time to find a polyfill for older browsers. You might want to check Modernizr if this is needed in your case.
PAGELOAD
try {
window.history.pushState({
url: '',
id: this.content.data("id"), // html data-id
label: this.content.data("label") // html data-label
}, "just content or your label variable", window.location.href);
} catch (e) {
console.log(e);
}
EVENT HANDLERS
An object filled with default information
var obj = {
url: settings.assetsPath, // this came from php
lang: settings.language, // this came from php
historyData: {}
};
Bind the history.pushState() trigger. In my case a delegate since I have dynamic elements on the page.
// click a trigger -> push state
this.root.on("click", ".cssSelector", function (ev) {
var path = [],
urlChunk = document.location.pathname; // to follow your example
// some data-attributes you need? like id or label
// override obj.historyData
obj.historyData.id = $(ev.currentTarget).data("id");
// create a relative path for security reasons
path.push("..", obj.lang, label, urlChunk);
path = path.join("/");
// attempt to push a state
try {
window.history.pushState(obj.historyData, label, path);
this.back.fadeIn();
this.showContent(obj.historyData.id);
} catch (e) {
console.log(e);
}
});
Bind the history.back() event to a custom button, link or something.
I used .preventDefault() since my button is a link.
// click back arrow -> history
this.back.on("click", function (ev) {
ev.preventDefault();
window.history.back();
});
When history pops back -> check for a pushed state unless it was the first attempt
$(window).on("popstate", function (ev) {
var originalState = ev.originalEvent.state || obj.historyData;
if (!originalState) {
// no history, hide the back button or something
this.back.fadeOut();
return;
} else {
// do something
this.showContent(obj.historyData.id);
}
});
Using object literals as a parameter is handy to pass your id's. Then you can use one function showContent(id).
Wherever I've used this it's nothing more than a jQuery object/function, stored inside an IIFE.
Please note I put these scripts together from my implementation combined with some ideas from your initial request. So hopefully this gives you some new ideas ;)
I am using a omniture jasavscript for Site Catalyst.
In which, I am populating the required variables onclick of a link.
But the problem is I get a multiple (2) tracking on a single click, which is not the ideal behaviour. in these 2 tracking, The FIrst one I get is the old one and right after that I get the second latest tracking.
It seems like it is using the cache memory.
UPDATE
I tried reinitializing the object by using var s = {}; before and after the use of s.tl('this','e','',null);
But it didn't worked
Could someone suggest how it can be rectified.
Without seeing any code I can only speculate, but my guess is the additional hit is from SiteCatalyst's auto-link tracking - either an exit link because the target URL is not listed in linkInternalFilters, or a download link because the target URL ends with something listed in linkDownloadFileTypes.
I suspect, given the 'e' argument of your s.tl() example, that the link is an exit link. So on that note.. perhaps the solution here is to piggyback off the auto-exit-link tracking, instead of making your own s.tl() call. Adobe has a plugin called exitLinkHandler that will let you trigger additional variables whenever the auto-exit-link tracking occurs.
Here is the plugin:
/*
* Plugin: exitLinkHandler 0.5 - identify and report exit links
*/
s.exitLinkHandler=new Function("p",""
+"var s=this,h=s.p_gh(),n='linkInternalFilters',i,t;if(!h||(s.linkTyp"
+"e&&(h||s.linkName)))return '';i=h.indexOf('?');t=s[n];s[n]=p?p:t;h="
+"s.linkLeaveQueryString||i<0?h:h.substring(0,i);if(s.lt(h)=='e')s.li"
+"nkType='e';else h='';s[n]=t;return h;");
Within your s_doPlugins function, add the following:
s.url = s.exitLinkHandler();
if (s.url) {
// pop your variables here. Don't forget to pop `linkTrackVars` and `linkTrackEvents`, same as you would have done before
}
Now, this will make your additional variables pop on any exit link triggered. If you want it to only trigger on certain URL matches, or only on a specific match, you can do this several ways, depending on your needs:
If you only need to do a general substring match, you can pass some
or all of the target URL as the first argument for
s.exitLinkHandler() and it will match the passed argument against
the target URL.
If this isn't good enough, within the if(s.url) condition, you can
perform your own matching (e.g. regex matching) against the target
URL using s.url.
If you need to target by some DOM attribute of the link, within the
condition, s.eo is an object reference to the link that was
clicked, so you can write your own conditions around that.
Option 1
Omniture does not track links with # as exit links so you can do something like:
Search
<script>
(function (){
'use strict';
var links = document.querySelectorAll('.prepended-with-hash-for-tracking');
var track = function(e) {
e.preventDefault();
var link = e.currentTarget;
var url = link.href;
var trackingMessage = link.getAttribute('data-track-msg');
// Remove the hash.
if (url[0] === '#') {
url = url.substr(1);
}
// Track in omniture.
var s = s_gi('InsertYourRSID');
s.tl(link, 'o', trackingMessage, null, function(){
window.location.href = url;
});
};
for (var i = 0, len = links.length; i < len; i++) {
links[i].addEventListener('click', track, false);
}
})();
</script>
Option 2
Another work-a-round is to set s.linkLeaveQueryString = true; and then append the url with a query parameter containing your domain name which matches a string in s.linkInternalFilters. e.g. Share
Option 3
Disable omniture's default external link tracking by setting s.trackExternalLinks=false; and then you can handle all external links with an event handler that calls s.tl() with JavaScript similar to option 1.
I would recommend option 3.