Skip to content

Commit 98231b8

Browse files
committed
replace_url_with_links
url utility functions checkbox widget payload:after event api_call_back_*:after event
1 parent 9443b8b commit 98231b8

File tree

1 file changed

+184
-22
lines changed

1 file changed

+184
-22
lines changed

src/twentyc.rest.js

Lines changed: 184 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* global jQuery, twentyc */
21
(function(twentyc, $) {
32

43
/**
@@ -20,6 +19,30 @@
2019
}
2120
})(jQuery);
2221

22+
/**
23+
* takes a jquery result and replaces all plain text occurences
24+
* of email addresses and urls with links
25+
*
26+
* @method replace_urls_with_links
27+
*/
28+
29+
function replace_urls_with_links(jQueryResult) {
30+
jQueryResult.contents().filter(function() {
31+
return this.nodeName != "A";
32+
}).each(function() {
33+
const text = $(this).text();
34+
const urlRegex = /((https?:\/\/[^\s]+)|(\S+@\S+))/g;
35+
const html = text.replace(urlRegex, (match) => {
36+
const emailRegex = /\S+@\S+/;
37+
if (emailRegex.test(match)) {
38+
return `<a href="mailto:${match}">${match}</a>`;
39+
}
40+
return `<a href="${match}">${match}</a>`;
41+
});
42+
$(this).replaceWith(html);
43+
});
44+
}
45+
2346
/**
2447
* namespace for twentyc.rest
2548
*/
@@ -46,9 +69,77 @@ twentyc.rest = {
4669
*/
4770

4871
csrf : ""
72+
},
73+
74+
/**
75+
* object holding URL utility functions
76+
* @property url
77+
* @type Object
78+
* @namespace twentyc.rest
79+
*/
80+
81+
url: {
82+
/**
83+
* Trims leading and trailing slashes from the given endpoint.
84+
*
85+
* @method trim_endpoint
86+
* @param {String} endpoint - The endpoint string to trim.
87+
* @return {String} The trimmed endpoint string.
88+
*/
89+
trim_endpoint: function (endpoint) {
90+
// urljoin is not guaranteed to strip trailing double slashes on
91+
// either side of the endpoint, so we do it manually
92+
return endpoint.replace(/^\/+|\/+$/g, "");
93+
},
94+
95+
/**
96+
* Joins URL parts, removing extra slashes at the edges of the parts.
97+
*
98+
* @method url_join
99+
* @param {String} left - The leftmost URL part.
100+
* @param {...String} args - The remaining URL parts.
101+
* @return {String} The joined URL string with removed extra slashes.
102+
*/
103+
url_join: function (left, ...args) {
104+
// Simplified urljoin that gets rid of extra / at the edges
105+
// of parts
106+
107+
let right = [];
108+
let trailing_slash = !twentyc.rest.no_end_slash;
109+
110+
// trim left
111+
112+
left = left.replace(/\/+$/g, "");
113+
114+
for (const parts of args) {
115+
right = right.concat(
116+
parts
117+
.split("/")
118+
.filter((part) => part)
119+
.map((part) => this.trim_endpoint(part))
120+
);
121+
}
122+
123+
if(!right.length)
124+
return trailing_slash ? `${left}/` : left;
125+
126+
right = right.join("/");
127+
128+
if (!left) {
129+
return trailing_slash ? `/${right}/` : `/${right}`;
130+
}
131+
132+
const joinedUrl = `${left.replace(/\/$/, "")}/${right}`;
133+
134+
return trailing_slash ? `${joinedUrl}/` : joinedUrl;
135+
}
136+
49137
}
138+
50139
};
51140

141+
142+
52143
/**
53144
* Wrapper for API responses
54145
* @class Response
@@ -392,12 +483,10 @@ twentyc.rest.Client = twentyc.cls.define(
392483
*/
393484

394485
endpoint_url : function(endpoint) {
395-
396-
var end_slash = (twentyc.rest.no_end_slash ? "" : "/");
397-
398486
if(!endpoint)
399-
return this.base_url+end_slash;
400-
return this.base_url+"/"+endpoint+end_slash;
487+
return twentyc.rest.url.url_join(this.base_url);
488+
489+
return twentyc.rest.url.url_join(this.base_url, endpoint);
401490
},
402491

403492
/**
@@ -468,16 +557,18 @@ twentyc.rest.Client = twentyc.cls.define(
468557
* @returns {Promise}
469558
*/
470559

471-
write : function(endpoint, data, method) {
560+
write : function(endpoint, data, method, url = null) {
561+
url = url || this.endpoint_url(endpoint);
472562
method = method.toLowerCase();
563+
473564
$(this).trigger("api-request:before", [endpoint,data,method])
474565
$(this).trigger("api-write:before", [endpoint,data,method])
475566
$(this).trigger("api-"+method+":before", [endpoint,data])
476567
var request = new Promise(function(resolve, reject) {
477568
$.ajax({
478569
dataType : "json",
479570
method : method.toUpperCase(),
480-
url : this.format_request_url(this.endpoint_url(endpoint), method),
571+
url : this.format_request_url(url, method),
481572
data : this.encode(data),
482573
headers : {
483574
"Content-Type" : "application/json",
@@ -761,9 +852,12 @@ twentyc.rest.Widget = twentyc.cls.extend(
761852
for(i = 0; i < errors.length; i++) {
762853
$(twentyc.rest).trigger("non-field-error", [errors[i], errors, i, error_node, this]);
763854
if(errors[i])
764-
error_node.append($('<div>').addClass("non-field-error").text(errors[i]))
855+
error_node.append($('<div>').addClass("non-field-error").text(errors[i]));
765856
}
766-
this.element.prepend(error_node)
857+
858+
replace_urls_with_links(error_node);
859+
860+
this.element.prepend(error_node);
767861
},
768862

769863
/**
@@ -794,8 +888,17 @@ twentyc.rest.Widget = twentyc.cls.extend(
794888
data[input.attr("name")] = input.val();
795889
}
796890
}
891+
892+
// treat blank values as null where necessary
893+
894+
if(input.data("blank-as-null") == "yes" && input.val() == "") {
895+
data[input.attr("name")] = null;
896+
}
797897
});
798898
});
899+
900+
$(this).trigger("payload:after", [data]);
901+
799902
return data;
800903

801904
},
@@ -821,7 +924,7 @@ twentyc.rest.Widget = twentyc.cls.extend(
821924

822925
var k, tag, val, formatter;
823926
for(k in data) {
824-
formatter = this.formatters[k];
927+
var formatter = this.formatters[k];
825928
var col_element = element.find('[data-field="'+k+'"]');
826929

827930
if(col_element.length)
@@ -1173,6 +1276,59 @@ twentyc.rest.Input = twentyc.cls.extend(
11731276
twentyc.rest.Widget
11741277
);
11751278

1279+
/**
1280+
* Checkbox widget
1281+
*
1282+
* Wires a html element (input type="checkbox") to the API
1283+
*
1284+
* The select element should have the following attributes set
1285+
*
1286+
* # required
1287+
*
1288+
* - data-api-base: api root or full path to endpoint
1289+
*
1290+
* @class Button
1291+
* @extends twentyc.rest.Input
1292+
* @namespace twentyc.rest
1293+
* @param {jQuery result} jq jquery result holding the button element
1294+
*/
1295+
1296+
1297+
twentyc.rest.Checkbox = twentyc.cls.extend(
1298+
"Checkbox",
1299+
{
1300+
payload : function() {
1301+
var pl = {};
1302+
pl[this.element.attr('name')] = (this.element.prop("checked") ? true : false);
1303+
return pl;
1304+
},
1305+
bind : function(jq) {
1306+
this.Widget_bind(jq);
1307+
this.method = jq.data("api-method") || "POST";
1308+
1309+
this.element.on("change", function(ev) {
1310+
1311+
var confirm_required = this.element.data("confirm");
1312+
if(confirm_required && !confirm(confirm_required))
1313+
return;
1314+
1315+
this.clear_errors();
1316+
1317+
var action = this.action;
1318+
var fn = this[this.method.toLowerCase()].bind(this);
1319+
1320+
fn(action, this.payload()).then(
1321+
this.post_success.bind(this),
1322+
this.post_failure.bind(this)
1323+
);
1324+
}.bind(this));
1325+
1326+
}
1327+
},
1328+
twentyc.rest.Input
1329+
);
1330+
1331+
11761332
/**
11771333
* Button widget
11781334
*
@@ -1315,6 +1471,7 @@ twentyc.rest.Select = twentyc.cls.extend(
13151471
$(this.proxy_data).find('option').each(function() {
13161472
select.append($(this).clone());
13171473
});
1474+
$(this).trigger("load:after", [select, {}, this]);
13181475
return new Promise((resolve, reject) => {
13191476
resolve();
13201477
});
@@ -1361,6 +1518,8 @@ twentyc.rest.Select = twentyc.cls.extend(
13611518
var selected_field = this.selected_field
13621519
var widget = this;
13631520

1521+
var old_val = select.val();
1522+
13641523
select.empty();
13651524

13661525
if(this.null_option) {
@@ -1379,8 +1538,11 @@ twentyc.rest.Select = twentyc.cls.extend(
13791538
select.append(opt);
13801539
});
13811540

1382-
if(select_this)
1541+
if(select_this) {
13831542
select.val(select_this);
1543+
if(select_this != old_val)
1544+
select.trigger("change", []);
1545+
}
13841546

13851547
$(this).trigger("load:after", [select, response.content.data, this]);
13861548
}.bind(this));
@@ -1587,7 +1749,7 @@ twentyc.rest.List = twentyc.cls.extend(
15871749
response.rows(function(row, idx) {
15881750
this.insert(row);
15891751
}.bind(this));
1590-
$(this).trigger("load:after");
1752+
$(this).trigger("load:after", [response]);
15911753
return
15921754
}.bind(this));
15931755
},
@@ -1733,6 +1895,7 @@ twentyc.rest.List = twentyc.cls.extend(
17331895
var method = ($(this).data("api-method") || "POST").toLowerCase();
17341896
var action = $(this).data("api-action").toLowerCase();
17351897
var callback = $(this).data("api-callback");
1898+
var callback_name = $(this).data("api-callback");
17361899
var confirm_set = $(this).data("confirm")
17371900

17381901
if(callback)
@@ -1743,13 +1906,15 @@ twentyc.rest.List = twentyc.cls.extend(
17431906
return;
17441907
var apiobj = row.data("apiobject")
17451908
var _action = action.replace(
1746-
/\{([^{}]+)\}/,
1909+
/\{([^\{\}]+)\}/,
17471910
(match, p1, offset, string) => {
17481911
return apiobj[p1];
17491912
}
17501913
)
17511914
widget[method](_action, row.data("apiobject")).then(
17521915
callback, widget.action_failure.bind(widget)
1916+
).then(
1917+
$(widget).trigger("api_callback_"+callback_name+":after")
17531918
);
17541919
});
17551920
});
@@ -1801,9 +1966,7 @@ twentyc.rest.List = twentyc.cls.extend(
18011966
Specific to django-rest-framework: we add "ordering" as a query
18021967
parameter to the API calls
18031968
*/
1804-
this.payload = function(){return {ordering: this.ordering}}
1805-
1806-
console.log("sort init", this);
1969+
this.payload = function(){return {ordering: this.ordering};};
18071970
},
18081971

18091972
sort: function(target, secondary) {
@@ -1813,7 +1976,7 @@ twentyc.rest.List = twentyc.cls.extend(
18131976
} else {
18141977
this.sort_target = target;
18151978
this.sort_asc = true;
1816-
}
1979+
};
18171980
this.load();
18181981
},
18191982

@@ -1901,12 +2064,11 @@ twentyc.rest.PermissionsForm = twentyc.cls.extend(
19012064

19022065
set_flag_values : function(flags) {
19032066
this.element.find('input[data-permission-flag]').each(function() {
1904-
var value, flag_name = $(this).data("permission-flag")
2067+
var flag_name = $(this).data("permission-flag")
19052068
if(flag_name.length == 1) {
1906-
value = (flags.perms.indexOf(flag_name) > -1)
2069+
var value = (flags.perms.indexOf(flag_name) > -1)
19072070
} else {
1908-
var i;
1909-
value = true;
2071+
var i, value = true;
19102072
for(i = 0; i < flag_name.length; i++) {
19112073
if(flags.perms.indexOf(flag_name.charAt(i)) == -1)
19122074
value = false;

0 commit comments

Comments
 (0)