From 1b97996ef8907726f12d3752faa4434df156d9ac Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 7 Sep 2017 12:26:05 +0200 Subject: [PATCH 01/15] implement util.once --- www/common/common-util.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/www/common/common-util.js b/www/common/common-util.js index 8cbde420c..e42a0249c 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -170,5 +170,15 @@ define([], function () { return parts[0]; }; + /* for wrapping async functions such that they can only be called once */ + Util.once = function (f) { + var called; + return function () { + if (called) { return; } + called = true; + f.apply(this, Array.prototype.slice.call(arguments)); + }; + }; + return Util; }); From 15e24ebe4f331fd7770756fbcc0f32eb27f31638 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 7 Sep 2017 12:45:07 +0200 Subject: [PATCH 02/15] ui for hashtag picker --- bower.json | 3 +- .../src/less2/include/alertify.less | 15 +- .../src/less2/include/tokenfield.less | 94 ++++++++++++ www/common/common-interface.js | 137 +++++++++++++++++- www/common/cryptpad-common.js | 16 ++ 5 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 customize.dist/src/less2/include/tokenfield.less diff --git a/bower.json b/bower.json index 9c88d0033..cb8d01f07 100644 --- a/bower.json +++ b/bower.json @@ -41,6 +41,7 @@ "bootstrap": "#v4.0.0-alpha.6", "diff-dom": "2.1.1", "nthen": "^0.1.5", - "open-sans-fontface": "^1.4.2" + "open-sans-fontface": "^1.4.2", + "bootstrap-tokenfield": "^0.12.1" } } diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 8dd7fc0cb..f9f2c28b8 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -85,6 +85,20 @@ .dialog { padding: 12px; +/* + div.tokenfield { + .token { + //border: 1px solid red; + //color: red; + } + + color: @colortheme_light-base; + background-color: @alertify-dialog-bg; + + input[id$="tokenfield"][type="text"].token-input { + background-color: @alertify-dialog-bg !important; + } + }*/ } .dialog, .alert { @@ -308,6 +322,5 @@ pointer-events: auto; } } - } } diff --git a/customize.dist/src/less2/include/tokenfield.less b/customize.dist/src/less2/include/tokenfield.less new file mode 100644 index 000000000..2479e3330 --- /dev/null +++ b/customize.dist/src/less2/include/tokenfield.less @@ -0,0 +1,94 @@ +.tokenfield_main () { + .tokenfield { + .unselectable () { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .unselectable(); + height: auto; + min-height: 34px; + padding-bottom: 0px; + &.focus { + border-color: #66afe9; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); + } + .token { + box-sizing: border-box; + border-radius: 3px; + display: inline-block; + border: 1px solid #d9d9d9; + background-color: #ededed; + white-space: nowrap; + margin: -1px 5px 5px 0; + vertical-align: center; + cursor: default; + + color: #222; + + &:hover { + border-color: #b9b9b9; + } + &.invalid { + background: none; + border: 1px solid transparent; + border-radius: 0; + border-bottom: 1px dotted #d9534f; + } + &.invalid.active { + background: #ededed; + border: 1px solid #ededed; + border-radius: 3px; + } + .token-label { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 4px; + vertical-align: center; + } + .close { + font-family: Arial; + display: inline-block; + line-height: 100%; + font-size: 1.1em; + margin-left: 5px; + float: none; + height: 100%; + vertical-align: center; + padding-right: 4px; + } + &.active { + border-color: #52a8ec; + border-color: rgba(82, 168, 236, 0.8); + } + &.duplicate { + border-color: #ebccd1; + } + } + .token-input { + background: none; + width: 0%; //60px; + min-width: 60px; + border: 0; + padding: 0; + margin-bottom: 6px; + box-shadow: none; + max-width: 100%; + &:focus { + border-color: transparent; + outline: 0; + box-shadow: none; + } + } + &.disabled { + cursor: not-allowed; + background-color: #eeeeee; + } + } +} diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 745a2b5aa..a8122dcc6 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -7,9 +7,10 @@ define([ '/common/notify.js', '/common/visible.js', '/common/tippy.min.js', + '/common/hyperscript.js', + '/bower_components/bootstrap-tokenfield/dist/bootstrap-tokenfield.js', 'css!/common/tippy.css', -], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible, Tippy) { - +], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible, Tippy, h) { var UI = {}; /* @@ -20,11 +21,17 @@ define([ // set notification timeout Alertify._$$alertify.delay = AppConfig.notificationTimeout || 5000; - var findCancelButton = UI.findCancelButton = function () { + var findCancelButton = UI.findCancelButton = function (root) { + if (root) { + return $(root).find('button.cancel').last(); + } return $('button.cancel').last(); }; - var findOKButton = UI.findOKButton = function () { + var findOKButton = UI.findOKButton = function (root) { + if (root) { + return $(root).find('button.ok').last(); + } return $('button.ok').last(); }; @@ -33,7 +40,6 @@ define([ switch (e.which) { case 27: // cancel if (typeof(no) === 'function') { no(e); } - no(); break; case 13: // enter if (typeof(yes) === 'function') { yes(e); } @@ -363,5 +369,126 @@ define([ } }; + UI.tokenField = function (target) { + var t = { + element: target || h('input'), + }; + var $t = t.tokenfield = $(t.element).tokenfield(); + t.getTokens = function () { + return $t.tokenfield('getTokens').map(function (token) { + return token.value; + }); + }; + + t.preventDuplicates = function (cb) { + $t.on('tokenfield:createtoken', function (ev) { + var val; + if (t.getTokens().some(function (t) { + if (t === ev.attrs.value) { return ((val = t)); } + })) { + ev.preventDefault(); + if (typeof(cb) === 'function') { cb(val); } + } + }); + return t; + }; + + t.setTokens = function (tokens) { + $t.tokenfield('setTokens', + tokens.map(function (token) { + return { + value: token, + label: token, + }; + })); + }; + + t.focus = function () { + var $temp = $t.closest('.tokenfield').find('.token-input'); + $temp.css('width', '20%'); + $t.tokenfield('focusInput', $temp[0]); + }; + + return t; + }; + + var dialog = UI.dialog = {}; + dialog.okButton = function () { + return h('button.ok', { tabindex: '2', }, Messages.okButton); + }; + + dialog.cancelButton = function () { + return h('button.cancel', { tabindex: '1'}, Messages.cancelButton); + }; + + dialog.message = function (text) { + return h('p.message', text); + }; + + dialog.textInput = function (opt) { + return h('input', opt || { + placeholder: '', + type: 'text', + 'class': 'cp-text-input', + }); + }; + + dialog.nav = function (content) { + return h('nav', content || [ + dialog.cancelButton(), + dialog.okButton(), + ]); + }; + + dialog.frame = function (content) { + return h('div.alertify', [ + h('div.dialog', [ + h('div', content), + ]) + ]); + }; + + dialog.tagPrompt = function (tags, cb) { + var input = dialog.textInput(); + + var tagger = dialog.frame([ + dialog.message('make some tags'), // TODO translate + input, + dialog.nav(), + ]); + + var field = UI.tokenField(input).preventDuplicates(function (val) { + UI.warn('Duplicate tag: ' + val); // TODO translate + }); + + var close = Util.once(function () { + var $t = $(tagger).fadeOut(150, function () { $t.remove(); }); + }); + + var listener = listenForKeys(function () {}, function () { + close(); + stopListening(listener); + }); + + var CB = Util.once(cb); + findOKButton(tagger).click(function () { + var tokens = field.getTokens(); + close(); + CB(tokens); + }); + findCancelButton(tagger).click(function () { + close(); + CB(null); + }); + + // :( + setTimeout(function () { + field.setTokens(tags); + field.focus(); + }); + + return tagger; + }; + return UI; }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 8b32f12d3..cd817583b 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -81,6 +81,8 @@ define([ common.addTooltips = UI.addTooltips; common.clearTooltips = UI.clearTooltips; common.importContent = UI.importContent; + common.tokenField = UI.tokenField; + common.dialog = UI.dialog; // import common utilities for export common.find = Util.find; @@ -1380,6 +1382,20 @@ define([ }) .click(prepareFeedback(type)); break; + case 'hashtag': + button = $('