From 5e3bd08eff9f45efbfefcdf224e2c45de616944c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Mon, 3 Feb 2025 14:28:40 +0100 Subject: [PATCH 01/12] fix: add opticons to replace missing glyphicons Glyphicons where removed from bootstrap in 4.0.0. Bootstrap bump from 3.x.x to 5.x.x leads at least to missing icons. --- package.json | 1 + yarn.lock | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 58e2e53cf1..a645820b3e 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "@gristlabs/pidusage": "2.0.17", "@gristlabs/sqlite3": "5.1.4-grist.8", "@popperjs/core": "2.11.8", + "@primer/octicons": "^19.14.0", "accept-language-parser": "1.5.0", "ace-builds": "1.23.3", "async-mutex": "0.2.4", diff --git a/yarn.lock b/yarn.lock index d85315042c..32ae6cc86e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -307,7 +307,12 @@ version "0.5.7" resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - + +"@emoji-mart/data@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c" + integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw== + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -423,11 +428,6 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== -"@emoji-mart/data@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c" - integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw== - "@eslint/eslintrc@^1.3.0": version "1.4.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" @@ -821,6 +821,13 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@primer/octicons@^19.14.0": + version "19.14.0" + resolved "https://registry.yarnpkg.com/@primer/octicons/-/octicons-19.14.0.tgz#6885734d30ae39978f07d103534a84258c82313e" + integrity sha512-9Ovw/xcUFHC/zbsNhr/Hkp1+m9XnNeQvnGHDHrI5vhlf6PRZVzSsdMnesV2xCzQh7jXP3EVRcaeXsUGlsZrfcA== + dependencies: + object-assign "^4.1.1" + "@sinonjs/commons@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" From 81f85cc5f4604114aea7157dbde1c53c5e943510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Mon, 3 Feb 2025 16:05:38 +0100 Subject: [PATCH 02/12] Revert "fix: add opticons to replace missing glyphicons" This reverts commit 5e3bd08eff9f45efbfefcdf224e2c45de616944c. --- package.json | 1 - yarn.lock | 19 ++++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index a645820b3e..58e2e53cf1 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,6 @@ "@gristlabs/pidusage": "2.0.17", "@gristlabs/sqlite3": "5.1.4-grist.8", "@popperjs/core": "2.11.8", - "@primer/octicons": "^19.14.0", "accept-language-parser": "1.5.0", "ace-builds": "1.23.3", "async-mutex": "0.2.4", diff --git a/yarn.lock b/yarn.lock index 32ae6cc86e..d85315042c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -307,12 +307,7 @@ version "0.5.7" resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@emoji-mart/data@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c" - integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw== - + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -428,6 +423,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== +"@emoji-mart/data@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.2.1.tgz#0ad70c662e3bc603e23e7d98413bd1e64c4fcb6c" + integrity sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw== + "@eslint/eslintrc@^1.3.0": version "1.4.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" @@ -821,13 +821,6 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@primer/octicons@^19.14.0": - version "19.14.0" - resolved "https://registry.yarnpkg.com/@primer/octicons/-/octicons-19.14.0.tgz#6885734d30ae39978f07d103534a84258c82313e" - integrity sha512-9Ovw/xcUFHC/zbsNhr/Hkp1+m9XnNeQvnGHDHrI5vhlf6PRZVzSsdMnesV2xCzQh7jXP3EVRcaeXsUGlsZrfcA== - dependencies: - object-assign "^4.1.1" - "@sinonjs/commons@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" From ffc5a23be01f4478615bc9e069e8c4e982235845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Tue, 4 Feb 2025 19:58:33 +0100 Subject: [PATCH 03/12] chore: Remove unused code calling glyphicon --- app/client/components/ColumnFilters.css | 9 -- app/client/components/DocConfigTab.js | 34 -------- app/client/components/GristDoc.ts | 3 - app/client/components/ValidationPanel.css | 29 ------- app/client/components/ValidationPanel.js | 100 ---------------------- app/client/lib/koForm.js | 14 --- 6 files changed, 189 deletions(-) delete mode 100644 app/client/components/DocConfigTab.js delete mode 100644 app/client/components/ValidationPanel.css delete mode 100644 app/client/components/ValidationPanel.js diff --git a/app/client/components/ColumnFilters.css b/app/client/components/ColumnFilters.css index 8f97d51d2c..44f08c738d 100644 --- a/app/client/components/ColumnFilters.css +++ b/app/client/components/ColumnFilters.css @@ -66,15 +66,6 @@ margin-right: 4px; } -.g-glyphicon-tristate { - position: absolute; - top: 4px; - left: 3px; - width: 5px; - height: 5px; - background: #606060; -} - .badge-inv { background-color: #ddd; color: #666; diff --git a/app/client/components/DocConfigTab.js b/app/client/components/DocConfigTab.js deleted file mode 100644 index 62a5b6cfca..0000000000 --- a/app/client/components/DocConfigTab.js +++ /dev/null @@ -1,34 +0,0 @@ -var dispose = require('../lib/dispose'); -var dom = require('../lib/dom'); -var ValidationPanel = require('./ValidationPanel'); - -/** - * Document level configuration settings. - * @param {Object} options.gristDoc A reference to the GristDoc object - * @param {Function} docName A knockout observable containing a String - */ -function DocConfigTab(options, docName) { - this.gristDoc = options.gristDoc; - - // Panel to configure validation rules. - this.validationPanel = this.autoDispose(ValidationPanel.create({gristDoc: this.gristDoc})); - - this.autoDispose( - this.gristDoc.addOptionsTab( - 'Validate Data', - dom('span.glyphicon.glyphicon-check'), - this.buildValidationsConfigDomObj(), - { 'shortLabel': 'Valid' } - ) - ); -} -dispose.makeDisposable(DocConfigTab); - -DocConfigTab.prototype.buildValidationsConfigDomObj = function() { - return [{ - 'buildDom': this.validationPanel.buildDom.bind(this.validationPanel), - 'keywords': ['document', 'validations', 'rules', 'validate'] - }]; -}; - -module.exports = DocConfigTab; diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index 9eff9dbcd9..1261ef43f9 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -11,7 +11,6 @@ import {CodeEditorPanel} from 'app/client/components/CodeEditorPanel'; import * as commands from 'app/client/components/commands'; import {CursorMonitor, ViewCursorPos} from "app/client/components/CursorMonitor"; import {DocComm} from 'app/client/components/DocComm'; -import * as DocConfigTab from 'app/client/components/DocConfigTab'; import {Drafts} from "app/client/components/Drafts"; import {EditorMonitor} from "app/client/components/EditorMonitor"; import {buildDefaultFormLayout} from 'app/client/components/Forms/FormView'; @@ -537,8 +536,6 @@ export class GristDoc extends DisposableWithEvents { this._handleTriggerQueueOverflowMessage(); - this.autoDispose(DocConfigTab.create({gristDoc: this})); - this.rightPanelTool = Computed.create(this, (use) => this._getToolContent(use(this._rightPanelTool))); this.comparison = options.comparison || null; diff --git a/app/client/components/ValidationPanel.css b/app/client/components/ValidationPanel.css deleted file mode 100644 index 7f9a1061c2..0000000000 --- a/app/client/components/ValidationPanel.css +++ /dev/null @@ -1,29 +0,0 @@ -.validation { - background-color: rgba(255, 255, 255, .5); - margin: 4px 8px 4px 1px; - padding: 3px 0; -} - -.validation_title { - position: relative; - width: 100%; - padding: 4px 8px; - margin-bottom: 10px; - border-bottom: 1px solid #E6E6E6; -} - -.validation_trash { - cursor: pointer; - color: #AAA; - font-size: 1.1rem; -} - -.validation_trash:hover { - color: black; -} - -.validation_formula { - width: 90%; - margin: 5px auto; - border: 1px solid #DDD; -} diff --git a/app/client/components/ValidationPanel.js b/app/client/components/ValidationPanel.js deleted file mode 100644 index c7b1dbee5a..0000000000 --- a/app/client/components/ValidationPanel.js +++ /dev/null @@ -1,100 +0,0 @@ -/* global $ */ -var ko = require('knockout'); -var dispose = require('../lib/dispose'); -var dom = require('../lib/dom'); -var kd = require('../lib/koDom'); -var kf = require('../lib/koForm'); -var AceEditor = require('./AceEditor'); -var {makeT} = require('app/client/lib/localization'); - -const t = makeT('ValidationPanel'); - -/** - * Document level configuration settings. - * @param {Object} options.gristDoc A reference to the GristDoc object - * @param {Function} docName A knockout observable containing a String - */ -function ValidationPanel(options) { - this.gristDoc = options.gristDoc; - - this.validationsTable = this.gristDoc.docModel.validations; - this.validations = this.autoDispose(this.validationsTable.createAllRowsModel('id')); - - this.docTables = this.autoDispose( - this.gristDoc.docModel.tables.createAllRowsModel('tableId')); - - this.tableChoices = this.autoDispose(this.docTables.map(function(table) { - return { label: table.tableId, value: table.id() }; - })); -} -dispose.makeDisposable(ValidationPanel); - - -ValidationPanel.prototype.onAddRule = function() { - this.validationsTable.sendTableAction(["AddRecord", null, { - tableRef: this.docTables.at(0).id(), - name: t("Rule {{length}}", {length: this.validations.peekLength + 1}), - formula: "" - }]) - .then(function() { - $('.validation_formula').last().find("input").focus(); - }); -}; - -ValidationPanel.prototype.onDeleteRule = function(rowId) { - this.validationsTable.sendTableAction(["RemoveRecord", rowId]); -}; - -ValidationPanel.prototype.buildDom = function() { - return [ - kf.row( - 1, kf.label('Validations'), - 1, kf.buttonGroup( - kf.button(this.onAddRule.bind(this), 'Add Rule', dom.testId("Validation_addRule")) - ) - ), - dom('div', - dom.testId("Validation_rules"), - kd.foreach(this.validations, validation => { - var editor = AceEditor.create({ observable: validation.formula }); - var editorUpToDate = ko.observable(true); - return dom('div.validation', - dom.autoDispose(editor), - dom('div.validation_title.flexhbox', - dom('div.validation_name', kf.editableLabel(validation.name)), - dom('div.flexitem'), - dom('div.validation_trash.glyphicon.glyphicon-remove', - dom.on('click', this.onDeleteRule.bind(this, validation.id())) - ) - ), - kf.row( - 1, dom('div.glyphicon.glyphicon-tag.config_icon'), - 8, kf.label('Table'), - 9, kf.select(validation.tableRef, this.tableChoices) - ), - dom('div.kf_elem.validation_formula', editor.buildDom(aceObj => { - editor.attachSaveCommand(); - aceObj.on('change', () => { - // Monitor whether the value mismatch is reflected by editorDiff - if ((editor.getValue() === validation.formula()) !== editorUpToDate()) { - editorUpToDate(!editorUpToDate()); - } - }); - aceObj.removeAllListeners('blur'); - })), - kf.row( - 2, '', - 1, kf.buttonGroup( - kf.button(() => editor.writeObservable(), - 'Apply', { title: t("Update formula (Shift+Enter)")}, - kd.toggleClass('disabled', editorUpToDate) - ) - ) - ) - ); - }) - ) - ]; -}; - -module.exports = ValidationPanel; diff --git a/app/client/lib/koForm.js b/app/client/lib/koForm.js index eb6ab0600d..d8aa16c52a 100644 --- a/app/client/lib/koForm.js +++ b/app/client/lib/koForm.js @@ -329,20 +329,6 @@ exports.selectSpinner = function(valueObservable, optionObservable) { return genSpinner(valueObservable, getNewValue, shouldDisable); }; -/** - * Creates an alignment selector linked to `valueObservable`. - */ -exports.alignmentSelector = function(valueObservable) { - return this.buttonSelect(valueObservable, - this.optionButton("left", dom('span.glyphicon.glyphicon-align-left'), - dom.testId('koForm_alignLeft')), - this.optionButton("center", dom('span.glyphicon.glyphicon-align-center'), - dom.testId('koForm_alignCenter')), - this.optionButton("right", dom('span.glyphicon.glyphicon-align-right'), - dom.testId('koForm_alignRight')) - ); -}; - /** * Label with a collapser triangle in front, which may be clicked to toggle `isCollapsedObs` * observable. From 4b12049422a60c032dd7326f700e4695b41252fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Tue, 4 Feb 2025 19:59:28 +0100 Subject: [PATCH 04/12] fix: replace paperclip icon Use the one in `static/icon/icons.css` --- app/client/widgets/AttachmentsWidget.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/client/widgets/AttachmentsWidget.ts b/app/client/widgets/AttachmentsWidget.ts index c360baf15d..5dd37ec194 100644 --- a/app/client/widgets/AttachmentsWidget.ts +++ b/app/client/widgets/AttachmentsWidget.ts @@ -253,4 +253,15 @@ const cssFileType = styled('div', ` &-small { font-size: ${vars.xxsmallFontSize}; } &-medium { font-size: ${vars.smallFontSize}; } &-large { font-size: ${vars.mediumFontSize}; } + + &::before { + display: block; + background-color: var(--grist-control-bg, --grist-theme-text, black); + content: ' '; + mask-image: var(--icon-FieldAttachment); + width: 14px; + height: 14px; + mask-size: contain; + mask-repeat: no-repeat; + } `); From f272998169572cae97bc4cb5725186e805c6b3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Tue, 4 Feb 2025 21:37:21 +0100 Subject: [PATCH 05/12] Wip: fix --- app/client/components/RecordLayoutEditor.js | 16 +++++++++++++++- app/client/widgets/AttachmentsWidget.ts | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/client/components/RecordLayoutEditor.js b/app/client/components/RecordLayoutEditor.js index 6b1339aafb..ff2a8203b4 100644 --- a/app/client/components/RecordLayoutEditor.js +++ b/app/client/components/RecordLayoutEditor.js @@ -125,7 +125,7 @@ RecordLayoutEditor.prototype.buildFinishButtons = function() { RecordLayoutEditor.prototype.buildLeafDom = function() { return dom('div.layout_grabbable.g_record_layout_editing', - dom('div.g_record_delete_field.glyphicon.glyphicon-eye-close', + cssIconEyeClose( dom.on('mousedown', (ev) => ev.stopPropagation()), dom.on('click', (ev, elem) => { ev.preventDefault(); @@ -151,4 +151,18 @@ const cssCollapseIcon = styled(icon, ` margin: -3px -2px -2px 2px; `); +const cssIconEyeClose = styled('div.g_record_delete_field', ` + &::before { + display: block; + background-color: var(--grist-color-dark-text); + content: ' '; + mask-image: var(--icon-EyeHide); + width: 14px; + height: 14px; + mask-size: contain; + mask-repeat: no-repeat; + } +` +); + module.exports = RecordLayoutEditor; diff --git a/app/client/widgets/AttachmentsWidget.ts b/app/client/widgets/AttachmentsWidget.ts index 5dd37ec194..02c7ca0115 100644 --- a/app/client/widgets/AttachmentsWidget.ts +++ b/app/client/widgets/AttachmentsWidget.ts @@ -190,7 +190,7 @@ const cssAttachmentWidget = styled('div', ` } `); -const cssAttachmentIcon = styled('div.glyphicon.glyphicon-paperclip', ` +const cssAttachmentIcon = styled('div', ` position: absolute; top: 2px; left: 5px; From bbc9a094843ef87b64d0ccdf4d7064877d9bdac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Cutzach?= Date: Wed, 5 Feb 2025 09:38:09 +0100 Subject: [PATCH 06/12] Remove unused code calling Glyphicons --- app/client/lib/koForm.js | 416 ++++++++------------------------- app/client/lib/multiselect.css | 37 --- app/client/lib/multiselect.js | 89 ------- 3 files changed, 100 insertions(+), 442 deletions(-) delete mode 100644 app/client/lib/multiselect.css delete mode 100644 app/client/lib/multiselect.js diff --git a/app/client/lib/koForm.js b/app/client/lib/koForm.js index d8aa16c52a..deef5a2a1a 100644 --- a/app/client/lib/koForm.js +++ b/app/client/lib/koForm.js @@ -9,20 +9,17 @@ // Use the browser globals in a way that allows replacing them with mocks in tests. var G = require('./browserGlobals').get('$', 'window', 'document'); -const identity = require('lodash/identity'); -const defaults = require('lodash/defaults'); const debounce = require('lodash/debounce'); -const pick = require('lodash/pick'); -var ko = require('knockout'); -var Promise = require('bluebird'); +const pick = require('lodash/pick'); +var ko = require('knockout'); var gutil = require('app/common/gutil'); var commands = require('../components/commands'); -var dom = require('./dom'); -var kd = require('./koDom'); -var koArray = require('./koArray'); +var dom = require('./dom'); +var kd = require('./koDom'); +var koArray = require('./koArray'); var modelUtil = require('../models/modelUtil'); @@ -33,9 +30,9 @@ var setSaveValue = modelUtil.setSaveValue; * Creates a button-looking div inside a buttonGroup; when clicked, clickFunc() will be called. * The button is not clickable if it contains the class 'disabled'. */ -exports.button = function(clickFunc, ...moreContentArgs) { +exports.button = function (clickFunc, ...moreContentArgs) { return dom('div.kf_button.flexitem', - dom.on('click', function() { + dom.on('click', function () { if (!this.classList.contains('disabled')) { clickFunc(); } @@ -48,9 +45,9 @@ exports.button = function(clickFunc, ...moreContentArgs) { * Creates a button with an accented appearance. * The button is not clickable if it contains the class 'disabled'. */ -exports.accentButton = function(clickFunc, ...moreContentArgs) { +exports.accentButton = function (clickFunc, ...moreContentArgs) { return this.button(clickFunc, - {'class': 'kf_button flexitem accent'}, + { 'class': 'kf_button flexitem accent' }, moreContentArgs ); }; @@ -59,9 +56,9 @@ exports.accentButton = function(clickFunc, ...moreContentArgs) { * Creates a button with a minimal appearance for use in prompts. * The button is not clickable if it contains the class 'disabled'. */ -exports.liteButton = function(clickFunc, ...moreContentArgs) { +exports.liteButton = function (clickFunc, ...moreContentArgs) { return this.button(clickFunc, - {'class': 'kf_button flexitem lite'}, + { 'class': 'kf_button flexitem lite' }, moreContentArgs ); }; @@ -70,9 +67,9 @@ exports.liteButton = function(clickFunc, ...moreContentArgs) { * Creates a bigger button with a logo, used for "sign in with google/github/etc" buttons. * The button is not clickable if it contains the class 'disabled'. */ -exports.logoButton = function(clickFunc, logoUrl, text, ...moreContentArgs) { +exports.logoButton = function (clickFunc, logoUrl, text, ...moreContentArgs) { return this.button(clickFunc, - {'class': 'kf_button kf_logo_button flexitem flexhbox'}, + { 'class': 'kf_button kf_logo_button flexitem flexhbox' }, dom('div.kf_btn_logo', { style: `background-image: url(${logoUrl})` }), dom('div.kf_btn_text', text), moreContentArgs @@ -82,7 +79,7 @@ exports.logoButton = function(clickFunc, logoUrl, text, ...moreContentArgs) { /** * Creates a button group. Arguments should be `button` and `checkButton` objects. */ -exports.buttonGroup = function(moreButtonArgs) { +exports.buttonGroup = function (moreButtonArgs) { return dom('div.kf_button_group.kf_elem.flexhbox', dom.fwdArgs(arguments, 0)); }; @@ -91,9 +88,9 @@ exports.buttonGroup = function(moreButtonArgs) { * Creates a button group with an accented appearance. * Arguments should be `button` and `checkButton` objects. */ -exports.accentButtonGroup = function(moreButtonArgs) { +exports.accentButtonGroup = function (moreButtonArgs) { return this.buttonGroup( - [{'class': 'kf_button_group kf_elem flexhbox accent'}].concat(dom.fwdArgs(arguments, 0)) + [{ 'class': 'kf_button_group kf_elem flexhbox accent' }].concat(dom.fwdArgs(arguments, 0)) ); }; @@ -101,19 +98,19 @@ exports.accentButtonGroup = function(moreButtonArgs) { * Creates a button group with a minimal appearance. * Arguments should be `button` and `checkButton` objects. */ -exports.liteButtonGroup = function(moreButtonArgs) { +exports.liteButtonGroup = function (moreButtonArgs) { return this.buttonGroup( - [{'class': 'kf_button_group kf_elem flexhbox lite'}].concat(dom.fwdArgs(arguments, 0)) + [{ 'class': 'kf_button_group kf_elem flexhbox lite' }].concat(dom.fwdArgs(arguments, 0)) ); }; /** * Creates a button-looking div that acts as a checkbox, toggling `valueObservable` on click. */ -exports.checkButton = function(valueObservable, moreContentArgs) { +exports.checkButton = function (valueObservable, moreContentArgs) { return dom('div.kf_button.kf_check_button.flexitem', kd.toggleClass('active', valueObservable), - dom.on('click', function() { + dom.on('click', function () { if (!this.classList.contains('disabled')) { setSaveValue(valueObservable, !valueObservable()); } @@ -128,10 +125,10 @@ exports.checkButton = function(valueObservable, moreContentArgs) { * TODO: checkButton and flatCheckButton are identical in function but differ in style and * class name conventions. We should reconcile them. */ -exports.flatCheckButton = function(valueObservable, moreContentArgs) { +exports.flatCheckButton = function (valueObservable, moreContentArgs) { return dom('div.flexnone', kd.toggleClass('mod-active', valueObservable), - dom.on('click', function() { + dom.on('click', function () { if (!this.classList.contains('mod-disabled')) { setSaveValue(valueObservable, !valueObservable()); } @@ -143,16 +140,16 @@ exports.flatCheckButton = function(valueObservable, moreContentArgs) { * Creates a group of buttons of which only one may be chosen. Arguments should be `optionButton` * objects. The single `valueObservable` reflects the value of the selected `optionButton`. */ -exports.buttonSelect = function(valueObservable, moreButtonArgs) { +exports.buttonSelect = function (valueObservable, moreButtonArgs) { var groupElem = dom('div.kf_button_group.kf_elem.flexhbox', dom.fwdArgs(arguments, 1)); // TODO: Is adding ":not(.disabled)" the best way to avoid execution? - G.$(groupElem).on('click', '.kf_button:not(.disabled)', function() { - setSaveValue(valueObservable, ko.utils.domData.get(this, 'kfOptionValue')); + G.$(groupElem).on('click', '.kf_button:not(.disabled)', function () { + setSaveValue(valueObservable, ko.utils.domData.get(this, 'kfOptionValue')); }); - kd.makeBinding(valueObservable, function(groupElem, value) { - Array.prototype.forEach.call(groupElem.querySelectorAll('.kf_button'), function(elem, i) { + kd.makeBinding(valueObservable, function (groupElem, value) { + Array.prototype.forEach.call(groupElem.querySelectorAll('.kf_button'), function (elem, i) { var v = ko.utils.domData.get(elem, 'kfOptionValue'); elem.classList.toggle('active', v === value); }); @@ -165,9 +162,9 @@ exports.buttonSelect = function(valueObservable, moreButtonArgs) { * Creates a button-like div to use inside a `buttonSelect` group. The `value` will become the * value of the `buttonSelect` observable when this button is selected. */ -exports.optionButton = function(value, moreContentArgs) { +exports.optionButton = function (value, moreContentArgs) { return dom('div.kf_button.flexitem', - function(elem) { ko.utils.domData.set(elem, 'kfOptionValue', value); }, + function (elem) { ko.utils.domData.set(elem, 'kfOptionValue', value); }, dom.fwdArgs(arguments, 1)); }; @@ -175,14 +172,14 @@ exports.optionButton = function(value, moreContentArgs) { * Creates a speech-bubble-like div intended to give more information and options affecting * its parent when hovered. */ -exports.toolTip = function(contentArgs) { +exports.toolTip = function (contentArgs) { return dom('div.kf_tooltip', dom('div.kf_tooltip_pointer'), dom('div.kf_tooltip_content', dom.fwdArgs(arguments, 0)), - dom.defer(function(elem) { + dom.defer(function (elem) { var elemWidth = elem.getBoundingClientRect().width; var parentRect = elem.parentNode.getBoundingClientRect(); - elem.style.left = (-elemWidth/2 + parentRect.width/2) + 'px'; + elem.style.left = (-elemWidth / 2 + parentRect.width / 2) + 'px'; elem.style.top = parentRect.height + 'px'; }) ); @@ -191,7 +188,7 @@ exports.toolTip = function(contentArgs) { /** * Creates a prompt to provide feedback or request more information in the sidepane. */ -exports.prompt = function(contentArgs) { +exports.prompt = function (contentArgs) { return dom('div.kf_prompt', dom('div.kf_prompt_pointer'), dom('div.kf_prompt_pointer_overlap'), @@ -202,13 +199,13 @@ exports.prompt = function(contentArgs) { /** * Checkbox which toggles `valueObservable`. Other arguments become part of the clickable label. */ -exports.checkbox = function(valueObservable, moreContentArgs) { +exports.checkbox = function (valueObservable, moreContentArgs) { return dom('label.kf_checkbox_label.kf_elem', - dom('input.kf_checkbox', {type: 'checkbox'}, - kd.makeBinding(valueObservable, function(elem, value) { + dom('input.kf_checkbox', { type: 'checkbox' }, + kd.makeBinding(valueObservable, function (elem, value) { elem.checked = value; }), - dom.on('change', function() { + dom.on('change', function () { setSaveValue(valueObservable, this.checked); }) ), @@ -221,11 +218,11 @@ exports.checkbox = function(valueObservable, moreContentArgs) { * matches the value, and selecting it sets the observable to the value. Other arguments become * part of the clickable label. */ -exports.radio = function(value, valueObservable, ...domArgs) { +exports.radio = function (value, valueObservable, ...domArgs) { return dom('label.kf_radio_label', - dom('input.kf_radio', {type: 'radio'}, + dom('input.kf_radio', { type: 'radio' }, kd.makeBinding(valueObservable, (elem, val) => { elem.checked = (val === value); }), - dom.on('change', function() { + dom.on('change', function () { if (this.checked) { setSaveValue(valueObservable, value); } @@ -292,7 +289,7 @@ function genSpinner(valueObservable, getNewValue, shouldDisable) { * @param {Number} optMin - Optional spinner lower bound * @param {Number} optMax - Optional spinner upper bound */ -exports.spinner = function(valueObservable, stepSizeObservable, optMin, optMax) { +exports.spinner = function (valueObservable, stepSizeObservable, optMin, optMax) { var max = optMax !== undefined ? optMax : Infinity; var min = optMin !== undefined ? optMin : -Infinity; @@ -316,7 +313,7 @@ exports.spinner = function(valueObservable, stepSizeObservable, optMin, optMax) * Creates a select spinner item to loop through the `optionObservable` array, * setting visible value to `valueObservable`. */ -exports.selectSpinner = function(valueObservable, optionObservable) { +exports.selectSpinner = function (valueObservable, optionObservable) { function getNewValue(value, direction) { const choices = optionObservable.peek(); const index = choices.indexOf(value); @@ -333,14 +330,14 @@ exports.selectSpinner = function(valueObservable, optionObservable) { * Label with a collapser triangle in front, which may be clicked to toggle `isCollapsedObs` * observable. */ -exports.collapserLabel = function(isCollapsedObs, moreContentArgs) { +exports.collapserLabel = function (isCollapsedObs, moreContentArgs) { return dom('div.kf_collapser.kf_elem', dom('span.kf_triangle_toggle', - kd.text(function() { + kd.text(function () { return isCollapsedObs() ? '\u25BA' : '\u25BC'; }) ), - dom.on('click', function() { + dom.on('click', function () { isCollapsedObs(!isCollapsedObs.peek()); }), dom.fwdArgs(arguments, 1)); @@ -362,7 +359,7 @@ exports.collapserLabel = function(isCollapsedObs, moreContentArgs) { * }); * Returns an array of two items: the always-shown element, and a div containing the rest. */ -exports.collapsible = function(contentFunc, isMountedCollapsed) { +exports.collapsible = function (contentFunc, isMountedCollapsed) { var isCollapsed = ko.observable(isMountedCollapsed === undefined ? true : isMountedCollapsed); var content = contentFunc(isCollapsed); return [ @@ -374,237 +371,24 @@ exports.collapsible = function(contentFunc, isMountedCollapsed) { }; -/** - * Creates a draggable list of rows. The contentArray argument must be an observable array. - * The callbackObj argument should include some or all of the following methods: - * reorder, remove, and receive. - * The reorder callback is executed if an item is dragged and dropped to a new position - * within the same collection or draggable container. The remove and receive callbacks - * are executed together only when an item from one collection is dropped on a different - * collection. The remove callback may be executed alone when users click on the "minus" icon - * for draggable items. The connectAllDraggables function must be called on draggables to - * enable the remove/receive operation between separate draggables. - * - * Each callback must update the respective model tied to the draggable component, - * or the equivalency between the UI and the observable array may be broken. When - * a method is implemented, but the callback cannot update the model for any reason - * (e.g., failure), then this failure should be communicated to the component either - * by throwing an Error in the callback, or by returning a rejected Promise. - * - * - * reorder(item, nextItem) - * @param {Object} item The item being relocated/moved - * @param {Object} nextItem The next item immediately following the new position, - * or null, when the item is moved to the end of the collection. - * remove(item) - * @param {Object} item The item that should be removed from the collection. - * @returns {Object} The item removed from the observable array. This - * value is passed to the receive function as the - * its item parameter. This value must include all the - * necessary data required for connected draggables - * to successfully insert the new value within their - * respective receive functions. - * receive(item, nextItem) - * @param {Object} item The item to insert in the collection. - * @param {Object} nextItem The next item from item's new position. This value - * will be null when item is moved to the end of the list. - * - * @param {Array} contentArray KoArray of model items - * @param {Function} itemCreateFunc Identical to koDom.foreach's itemCreateFunc, this - * function is called as `itemCreateFunc(item)` for each - * array element. Must return a single Node, or null or - * undefined to omit that node. - * @param {Object} options An object containing the reorder, remove, receive - * callback functions, and all other draggable configuration - * options -- - * @param {Boolean} options.removeButton Controls whether the clickable remove/minus icon is - * displayed. If true, this button triggers the remove - * function on click. - * @param {String} options.axis Determines if the list is displayed vertically 'y' or - * horizontally 'x'. - * @param {String} options.handle The handle of the draggable. Defaults to the element - * itself. - * @param {Boolean|Function} drag_indicator Include the drag indicator. Defaults to true. Accepts - * also a function that returns a dom element. In which - * case, it will be used to create the drag indicator. - * @returns {Node} The DOM Node for the draggable container - */ -exports.draggableList = function(contentArray, itemCreateFunc, options) { - options = options || {}; - defaults(options, { - removeButton: true, - axis: "y", - drag_indicator: true, - itemClass: 'kf_draggable__item' - }); - - var reorderFunc, removeFunc; - itemCreateFunc = itemCreateFunc || identity; - var list = dom('div.kf_drag_container', - function(elem) { - if (options.reorder) { - reorderFunc = Promise.method(options.reorder); - ko.utils.domData.set(elem, 'reorderFunc', reorderFunc); - } - if (options.remove) { - removeFunc = Promise.method(options.remove); - ko.utils.domData.set(elem, 'removeFunc', removeFunc); - } - if (options.receive) { - ko.utils.domData.set(elem, 'receiveFunc', Promise.method(options.receive)); - } - }, - kd.foreach(contentArray, item => { - var row = itemCreateFunc(item); - if (row) { - return dom('div.kf_draggable', - // Fix for JQueryUI bug where mousedown on draggable elements fail to blur - // active element. See: https://bugs.jqueryui.com/ticket/4261 - dom.on('mousedown', () => G.document.activeElement.blur()), - kd.toggleClass('kf_draggable--vertical', options.axis === 'y'), - kd.cssClass(options.itemClass), - (options.drag_indicator ? - (typeof options.drag_indicator === 'boolean' ? - dom('span.kf_drag_indicator.glyphicon.glyphicon-option-vertical') : - options.drag_indicator() - ) : null), - kd.domData('model', item), - kd.maybe(removeFunc !== undefined && options.removeButton, function() { - return dom('span.drag_delete.glyphicon.glyphicon-remove', - dom.on('click', function() { - removeFunc(item) - .catch(function(err) { - console.warn('Failed to remove item', err); - }); - }) - ); - }), - dom('span.kf_draggable_content.flexauto', row)); - } else { - return null; - } - }) - ); - - G.$(list).sortable({ - axis: options.axis, - tolerance: "pointer", - forcePlaceholderSize: true, - placeholder: 'kf_draggable__placeholder--' + (options.axis === 'x' ? 'horizontal' : 'vertical'), - handle: options.handle, - }); - if (reorderFunc === undefined) { - G.$(list).sortable("option", {disabled: true}); - } - - G.$(list).on('sortstart', function(e, ui) { - ko.utils.domData.set(ui.item[0], 'originalParent', ui.item.parent()); - ko.utils.domData.set(ui.item[0], 'originalPrev', ui.item.prev()); - }); - G.$(list).on('sortstop', function(e, ui) { - if (!ko.utils.domData.get(ui.item[0], 'crossedContainers')) { - handleReorderStop.bind(null, list).call(this, e, ui); - } else { - handleConnectedStop.call(list, e, ui); - } - }); - - return list; -}; - -function handleReorderStop(container, e, ui) { - var reorderFunc = ko.utils.domData.get(container, 'reorderFunc'); - var originalPrev = ko.utils.domData.get(ui.item[0], 'originalPrev'); - if (reorderFunc && !ui.item.prev().is(originalPrev)) { - var movingItem = ko.utils.domData.get(ui.item[0], 'model'); - reorderFunc(movingItem, getNextDraggableItemModel(ui.item)) - .catch(function(err) { - console.warn('Failed to reorder item', err); - G.$(container).sortable('cancel'); - }); - } - resetDraggedItem(ui.item[0]); -} - - -function handleConnectedStop(e, ui) { - var originalParent = ko.utils.domData.get(ui.item[0], 'originalParent'); - var removeOriginal = ko.utils.domData.get(originalParent[0], 'removeFunc'); - var receive = ko.utils.domData.get(ui.item.parent()[0], 'receiveFunc'); - - if (removeOriginal && receive) { - removeOriginal(ko.utils.domData.get(ui.item[0], 'model')) - .then(function(removedItem) { - return receive(removedItem, getNextDraggableItemModel(ui.item)) - .then(function() { - ui.item.remove(); - }) - .catch(revertRemovedItem.bind(null, ui, originalParent, removedItem)); - }) - .catch(function(err) { - console.warn('Error removing item', err); - G.$(originalParent).sortable('cancel'); - }) - .finally(function() { - resetDraggedItem(ui.item[0]); - }); - } else { - console.warn('Missing remove or receive'); - } -} - -function revertRemovedItem(ui, parent, item, err) { - console.warn('Error receiving item. Trying to return removed item.', err); - var originalReceiveFunc = ko.utils.domData.get(parent[0], 'receiveFunc'); - if (originalReceiveFunc) { - var originalPrev = ko.utils.domData.get(ui.item[0], 'originalPrev'); - var originalNextItem = originalPrev.length > 0 ? - getNextDraggableItemModel(originalPrev) : - getDraggableItemModel(parent.children('.kf_draggable').first()); - originalReceiveFunc(item, originalNextItem) - .catch(function(err) { - console.warn('Failed to receive item in original collection.', err); - }).finally(function() { - ui.item.remove(); - }); - } -} - -function getDraggableItemModel(elem) { - if (elem.length && elem.length > 0) { - return ko.utils.domData.get(elem[0], 'model'); - } - return null; -} - -function getNextDraggableItemModel(elem) { - return elem.next ? getDraggableItemModel(elem.next('.kf_draggable')) : null; -} - -function resetDraggedItem(elem) { - ko.utils.domData.set(elem, 'originalPrev', null); - ko.utils.domData.set(elem, 'originalParent', null); - ko.utils.domData.set(elem, 'crossedContainers', false); -} - function enableDraggableConnection(draggable) { - G.$(draggable).on('sortremove', function(e, ui) { + G.$(draggable).on('sortremove', function (e, ui) { ko.utils.domData.set(ui.item[0], 'crossedContainers', true); ko.utils.domData.set(ui.item[0], 'stopIndex', ui.item.index()); }); if (G.$(draggable).sortable("option", "disabled") && ( - ko.utils.domData.get(draggable, 'receiveFunc') || - ko.utils.domData.get(draggable, 'removeFunc') - )) { - G.$(draggable).sortable( "option", { disabled: false }); + ko.utils.domData.get(draggable, 'receiveFunc') || + ko.utils.domData.get(draggable, 'removeFunc') + )) { + G.$(draggable).sortable("option", { disabled: false }); } } function connectDraggableToClass(draggable, className) { enableDraggableConnection(draggable); G.$(draggable).addClass(className); - G.$(draggable).sortable("option", {connectWith: "." + className}); + G.$(draggable).sortable("option", { connectWith: "." + className }); } /** @@ -613,12 +397,12 @@ function connectDraggableToClass(draggable, className) { * @param {Object} draggableArgs 2 or more draggableList objects */ var connectedDraggables = 0; -exports.connectAllDraggables = function(draggableArgs) { +exports.connectAllDraggables = function (draggableArgs) { if (draggableArgs.length < 2) { console.warn('connectAllDraggables requires at least 2 draggable components'); } var className = "connected-draggable-" + connectedDraggables++; - for (var i=0; i