From faf1541020ecc3839470960c86cca636d837e9ad Mon Sep 17 00:00:00 2001 From: Irv Katz <60225276+exidy80@users.noreply.github.com> Date: Sun, 27 Apr 2025 09:10:50 -0400 Subject: [PATCH 01/21] upgrade quill-container --- app/components/quill-container.js | 156 +++++++------------ app/index.html | 7 +- app/templates/components/quill-container.hbs | 9 +- package-lock.json | 54 ++++++- package.json | 2 + 5 files changed, 122 insertions(+), 106 deletions(-) diff --git a/app/components/quill-container.js b/app/components/quill-container.js index 52ee18cfb..c75dc0ac1 100644 --- a/app/components/quill-container.js +++ b/app/components/quill-container.js @@ -1,15 +1,19 @@ -import { inject as service } from '@ember/service'; -import Component from '@ember/component'; -import $ from 'jquery'; +// app/components/quill-container.js -export default Component.extend({ - classNames: ['quill-container'], - utils: service('utility-methods'), +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { service } from '@ember/service'; +import Quill from 'quill'; - isEmpty: true, - isOverLengthLimit: false, +export default class QuillContainerComponent extends Component { + @service('utility-methods') utils; - defaultOptions: { + @tracked quillInstance = null; + @tracked isEmpty = true; + @tracked isOverLengthLimit = false; + + defaultOptions = { debug: 'false', modules: { toolbar: [ @@ -24,110 +28,68 @@ export default Component.extend({ theme: 'snow', placeholder: 'Type a detailed explanation here. This box will expand as you type.', - }, - - defaultMaxLength: 14680064, // 14MB - - didReceiveAttrs() { - this._super(); - let attrSectionId = this.attrSectionId; - if (!attrSectionId) { - this.set('sectionId', 'editor'); - } else { - this.set('sectionId', attrSectionId); - } + }; - let limit = this.maxLength || this.defaultMaxLength; - this.set('lengthLimit', limit); - }, + defaultMaxLength = 14680064; // 14MB - didInsertElement() { - let elId = this.elementId; - let selector = `#${elId} section`; + get sectionId() { + return this.args.attrSectionId || 'editor'; + } - let options; - if (!this.options) { - options = this.defaultOptions; - } - options.bounds = selector; + get lengthLimit() { + return this.args.maxLength || this.defaultMaxLength; + } - $(selector).ready(() => { - let quill = new window.Quill(selector, options); - this.set('quillInstance', quill); + @action + setupQuill(element) { + const options = this.args.options || this.defaultOptions; + options.bounds = element; - quill.on('text-change', (delta, oldDelta, source) => { - this.handleQuillChange(); - }); + const quill = new Quill(element, options); + this.quillInstance = quill; - this.handleStartingText(); - this.handleQuillChange(); - }); - this._super(...arguments); - }, + quill.on('text-change', this.handleQuillChange.bind(this)); - didUpdateAttrs() { - this._super(); this.handleStartingText(); - }, - - willDestroyElement() { - let quill = this.quillInstance; - if (quill) { - quill.off('text-change'); + this.handleQuillChange(); + } + + @action + teardownQuill() { + if (this.quillInstance) { + this.quillInstance.off('text-change'); + this.quillInstance = null; } - this._super(...arguments); - }, + } handleStartingText() { - let attrStartingText = this.startingText; - let startingText = - typeof attrStartingText === 'string' ? attrStartingText : ''; - $('.ql-editor').html(startingText); - }, - // Empty quill editor .html() property returns


- // For quill to not be empty, there must either be some text or a student - // must have uploaded an img so there must be an img tag - isQuillNonEmpty() { - let editor = $('.ql-editor'); - - if (!editor) { - return false; + if (this.quillInstance && typeof this.args.startingText === 'string') { + this.quillInstance.root.innerHTML = this.args.startingText; } - let editorText = editor.text(); - let trimmed = typeof editorText === 'string' ? editorText.trim() : ''; - - if (trimmed.length > 0) { - return true; - } - - let content = editor.html(); - if (content.includes(' this.lengthLimit; - this.set('isOverLengthLimit', isOverLengthLimit); + const isEmpty = this.isQuillNonEmpty(); + const isOverLengthLimit = replaced.length > this.lengthLimit; - this.onTextChange(replaced, isEmpty, isOverLengthLimit); - }, + this.isEmpty = !isEmpty; + this.isOverLengthLimit = isOverLengthLimit; - onTextChange(html, isEmpty, isOverLengthLimit) { - if (this.onEditorChange) { - this.onEditorChange(html, isEmpty, isOverLengthLimit); + if (this.args.onEditorChange) { + this.args.onEditorChange(replaced, !isEmpty, isOverLengthLimit); } - }, -}); + } + + isQuillNonEmpty() { + if (!this.quillInstance) return false; + const text = this.quillInstance.getText().trim(); + if (text.length > 0) return true; + const content = this.quillInstance.root.innerHTML; + return content.includes(' - + Encompass - - + + {{content-for "head"}} @@ -28,7 +28,6 @@ {{content-for "body"}} - diff --git a/app/templates/components/quill-container.hbs b/app/templates/components/quill-container.hbs index 59159b47c..ef13a494b 100644 --- a/app/templates/components/quill-container.hbs +++ b/app/templates/components/quill-container.hbs @@ -1 +1,8 @@ -
\ No newline at end of file + +
+
+
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 89c004aab..875c4bce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17763,8 +17763,7 @@ "fast-diff": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" }, "fast-glob": { "version": "3.3.2", @@ -20581,8 +20580,7 @@ "lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "lodash._baseassign": { "version": "3.2.0", @@ -20655,6 +20653,11 @@ "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", "integrity": "sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -20698,6 +20701,11 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -22662,6 +22670,11 @@ "readable-stream": "^2.1.5" } }, + "parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -23560,6 +23573,34 @@ "underscore.string": "~3.3.4" } }, + "quill": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", + "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "requires": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "dependencies": { + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + } + } + }, + "quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "requires": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + } + }, "qunit": { "version": "2.21.0", "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.21.0.tgz", @@ -27412,6 +27453,11 @@ } } }, + "validate.js": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/validate.js/-/validate.js-0.13.1.tgz", + "integrity": "sha512-PnFM3xiZ+kYmLyTiMgTYmU7ZHkjBZz2/+F0DaALc/uUtVzdCt1wAosvYJ5hFQi/hz8O4zb52FQhHZRC+uVkJ+g==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index cc766e705..fe4f8ebe0 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "nodemailer": "^6.9.13", "pdf2pic": "^2.2.4", "q": "^1.5.1", + "quill": "^2.0.3", "randomcolor": "^0.6.2", "request": "^2.88.2", "selectize": "^0.12.6", @@ -90,6 +91,7 @@ "typeahead.js": "^0.11.1", "underscore": "^1.13.6", "uuid": "^8.3.2", + "validate.js": "^0.13.1", "wordcloud": "^1.2.2" }, "devDependencies": { From 73768366cdd01274b4bb2c2f1c2f9a176fd5edf6 Mon Sep 17 00:00:00 2001 From: Irv Katz <60225276+exidy80@users.noreply.github.com> Date: Sun, 27 Apr 2025 09:39:53 -0400 Subject: [PATCH 02/21] move quill-container to component/ui folder --- app/components/problem-info.hbs | 2 +- app/components/problem-new.hbs | 2 +- .../ui}/quill-container.hbs | 0 app/components/{ => ui}/quill-container.js | 0 .../components/response-approver-reply.hbs | 4 ++-- .../components/response-mentor-reply.hbs | 2 +- app/templates/components/response-new.hbs | 2 +- .../components/response-submission-view.hbs | 21 +++++++++++++------ 8 files changed, 21 insertions(+), 12 deletions(-) rename app/{templates/components => components/ui}/quill-container.hbs (100%) rename app/components/{ => ui}/quill-container.js (100%) diff --git a/app/components/problem-info.hbs b/app/components/problem-info.hbs index 35f30156b..c4d228463 100644 --- a/app/components/problem-info.hbs +++ b/app/components/problem-info.hbs @@ -68,7 +68,7 @@
{{#if @isEditing}} {{! The starting text is from the argument; we change the local value }} - diff --git a/app/components/problem-new.hbs b/app/components/problem-new.hbs index 7eebeffa6..d497031c4 100644 --- a/app/components/problem-new.hbs +++ b/app/components/problem-new.hbs @@ -61,7 +61,7 @@ >

- diff --git a/app/templates/components/quill-container.hbs b/app/components/ui/quill-container.hbs similarity index 100% rename from app/templates/components/quill-container.hbs rename to app/components/ui/quill-container.hbs diff --git a/app/components/quill-container.js b/app/components/ui/quill-container.js similarity index 100% rename from app/components/quill-container.js rename to app/components/ui/quill-container.js diff --git a/app/templates/components/response-approver-reply.hbs b/app/templates/components/response-approver-reply.hbs index ce440c2bd..6fe3b1e96 100644 --- a/app/templates/components/response-approver-reply.hbs +++ b/app/templates/components/response-approver-reply.hbs @@ -44,7 +44,7 @@

{{replyHeadingText}}

- - {{#if this.isComposing}} -

Message

- {{/if}} {{#if this.isRevising}} - + {{else}} {{#if this.isLongExpanded}}
@@ -145,7 +145,8 @@ From 386fd1481e456b5c48086ac4114efc30dad9a7cc Mon Sep 17 00:00:00 2001 From: Irv Katz <60225276+exidy80@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:26:26 -0400 Subject: [PATCH 03/21] typeahead upgrade --- app/components/ui/twitter-typeahead.hbs | 21 +- app/components/ui/twitter-typeahead.js | 270 ++++++------------------ app/styles/_type-ahead-twitter.scss | 109 ++++------ package-lock.json | 14 +- package.json | 2 +- 5 files changed, 125 insertions(+), 291 deletions(-) diff --git a/app/components/ui/twitter-typeahead.hbs b/app/components/ui/twitter-typeahead.hbs index 0e45b6e2b..e51c18fcd 100644 --- a/app/components/ui/twitter-typeahead.hbs +++ b/app/components/ui/twitter-typeahead.hbs @@ -1,2 +1,21 @@ +
+ +
\ No newline at end of file diff --git a/app/components/ui/twitter-typeahead.js b/app/components/ui/twitter-typeahead.js index 533680d4f..7c48d9a7d 100644 --- a/app/components/ui/twitter-typeahead.js +++ b/app/components/ui/twitter-typeahead.js @@ -1,221 +1,79 @@ -import Ember from 'ember'; -import 'typeahead.js'; +// app/components/ui/typeahead.js -export default Ember.Component.extend({ - classNames: ['twitter-typeahead'], +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { debounce } from 'lodash-es'; - init() { - this._super(...arguments); - }, +export default class UiTypeaheadComponent extends Component { + @tracked query = ''; + @tracked isOpen = false; - didReceiveAttrs() { - this._super(...arguments); - let templates = { - ...(this.showNotFound && { notFound: this.showNotFound }), - ...(this.header && { header: this.header }), - ...(this.footer && { footer: this.footer }), - ...(this.pending && { pending: this.pending }), - }; + debouncedCloseDropdown = debounce(() => { + this.isOpen = false; + }, 200); - this.set('templates', templates); - }, + get placeholder() { + return this.args.placeholder || ''; + } - didInsertElement() { - this._super(...arguments); + get minLength() { + return this.args.minLength ?? 1; + } - let options = this.getOptions(); - let dataSetOptions = this.getDataSetOptions(); + get optionLabelPath() { + return this.args.optionLabelPath || 'id'; + } - let path = this.optionLabelPath; - let selectedValue = this.selectedValue; + get filteredList() { + const dataList = this.args.dataList || []; + const query = this.query.trim().toLowerCase(); - const that = this; - - $('.typeahead').typeahead(options, dataSetOptions); - - let startingValue; - - if (selectedValue === undefined || selectedValue === null) { - startingValue = ''; - } else if (typeof selectedValue === 'string') { - startingValue = selectedValue; - } else if (typeof selectedValue === 'object') { - let val = selectedValue.get(path); - if (val) { - startingValue = val; - } else { - startingValue = ''; - } - } - - $('.typeahead').typeahead('val', startingValue); - - $('.typeahead').on('typeahead:select', function (ev, suggestion) { - that.set('selectedValue', suggestion); - if (typeof that.onSelect === 'function') { - that[that.onSelect](suggestion); - } - if (that.get('allowMultiple')) { - that.$('.typeahead').typeahead('val', ''); - } - }); - - if (this.setSelectedValueOnChange) { - $('.typeahead').on('typeahead:change', function (ev, val) { - if (that.get('isDestroyed') || that.get('isDestroying')) { - return; - } - let selectedValue = that.get('selectedValue'); - let inputValue = that.$('.typeahead').typeahead('val'); - - if (selectedValue && typeof selectedValue === 'object') { - // check if text value in typeahead component is same as the object's path value - let str = selectedValue.get(path); - if (inputValue === str) { - return; - } - } else if (typeof selectedValue === 'string') { - if (selectedValue === inputValue) { - return; - } - } - - that.set('selectedValue', inputValue); - if (typeof that.onSelect === 'function') { - that[that.onSelect](inputValue); - } - if (that.get('allowMultiple')) { - that.$('.typeahead').typeahead('val', ''); - } - }); - } - }, - - willDestroyElement: function () { - this._super(...arguments); - $('.typeahead').typeahead('destroy'); - }, - - getOptions: function () { - let minLength = this.minLength; - let limit = this.limit; - let async = this.isAsync; - let hint = false; - let highlight = true; - - if ( - minLength === undefined || - minLength === null || - typeof minLength !== 'number' - ) { - minLength = 1; - } - - if (limit === undefined || limit === null || typeof limit !== 'number') { - limit = 5; + if (query.length < this.minLength) { + return []; } - let ret = { - minLength, - limit, - async, - hint, - highlight, - }; - - // name will default to random number if not provided - return ret; - }, - - getDataSetOptions: function () { - let name = this.listName; - let dataList = this.dataList; - let sourceFunction = this.sourceFunction; - let path = this.optionLabelPath; - let display = this.display; - let templates = this.templates; - let isAsync = this.isAsync; - let debounceTime = this.debounceTime; - - if (!debounceTime) { - debounceTime = 100; + return dataList + .map((item) => ({ + item, + label: this.getItemLabel(item), + })) + .filter(({ label }) => label?.toLowerCase().includes(query)); + } + + get hasResults() { + return this.isOpen && this.filteredList.length > 0; + } + + getItemLabel(item) { + const path = this.args.optionLabelPath || 'id'; + return typeof item.get === 'function' ? item.get(path) : item[path]; + } + + @action + updateQuery(event) { + this.query = event.target.value; + this.isOpen = true; + } + + @action + selectItem(entry) { + const item = entry.item; + if (this.args.onSelect) { + this.args.onSelect(item); } - let source; - - if (!sourceFunction || typeof sourceFunction !== 'function') { - source = this.substringMatcher.call(this, dataList); - } else if (!isAsync) { - source = sourceFunction.call(this, dataList); + if (this.args.allowMultiple) { + this.query = ''; + this.isOpen = false; } else { - source = window._.debounce(sourceFunction, debounceTime); - } - - if (!path || typeof path !== 'string') { - path = 'id'; - } - if (!display) { - display = function (suggestion) { - return suggestion.get(path); - }; - } - let ret = { - source, - display, - templates, - }; - if (name) { - ret.name = name; + this.query = entry.label || ''; + this.isOpen = false; } - return ret; - }, - - substringMatcher: function (data) { - // data should be array of ember objects - let path = this.optionLabelPath; - if (!path) { - path = 'id'; - } - - if (!data) { - data = []; - } - - let suggestions; - suggestions = data; - const that = this; - - return function findMatches(q, cb, async) { - var matches, substrRegex, filtered, pool; - if (that.get('allowMultiple')) { - // filter out already selected items - let selectedItems = that.get('selectedItems'); - if (!selectedItems) { - selectedItems = []; - } - filtered = suggestions.filter((item) => { - let alreadySelected = selectedItems.includes(item); - return !alreadySelected; - }); - } - - if (filtered) { - pool = filtered; - } else { - pool = suggestions; - } - // an array that will be populated with substring matches - matches = []; - // regex used to determine if a string contains the substring `q` - substrRegex = new RegExp(q, 'i'); - // iterate through the pool of strings and for any string that - // contains the substring `q`, add it to the `matches` array - matches = pool.filter((obj) => { - let str = obj.get(path); - return substrRegex.test(str); - }); + } - cb(matches); - }; - }, -}); + @action + handleBlur() { + this.debouncedCloseDropdown(); + } +} diff --git a/app/styles/_type-ahead-twitter.scss b/app/styles/_type-ahead-twitter.scss index 44506fe28..89f1350d9 100644 --- a/app/styles/_type-ahead-twitter.scss +++ b/app/styles/_type-ahead-twitter.scss @@ -1,75 +1,40 @@ - -.tt-menu, -.gist { - text-align: left; -} - -.typeahead, -.tt-query, -.tt-hint { - width: 100%; - padding: 8px 12px; - font-size: 0.8em; - line-height: 1.2em; - outline: none; -} - -.typeahead { - background-color: #fff; -} - -.typeahead:focus { - border: 2px solid #0097cf; -} - -.tt-query { - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} - -.tt-hint { - color: #999 -} - -.tt-menu { +.typeahead-container { + position: relative; width: 100%; - z-index:100; - margin: 12px 0; - padding: 8px 0; - background-color: #fff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - -webkit-border-radius: 8px; - -moz-border-radius: 8px; - border-radius: 8px; - -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); - -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); - box-shadow: 0 5px 10px rgba(0,0,0,.2); -} -.tt-suggestion { - padding: 3px 20px; - font-size: .8em; - line-height: 1.2em; -} -.tt-header { - padding: 3px 20px; - font-weight: bold; + .twitter-typeahead { + display: inline-block; + max-width: 200px; + width: 100%; + position: relative; + } + + .typeahead-input { + width: 100%; + padding: 0.5rem; + box-sizing: border-box; + } + + .typeahead-dropdown { + position: absolute; + width: 100%; + background: white; + border: 1px solid #ccc; + z-index: 10; + max-height: 200px; + overflow-y: auto; + margin-top: 2px; + padding: 0; + list-style: none; + + li { + padding: 0.5rem; + cursor: pointer; + + &:hover { + color: #eee; + background-color: #0097cf; + } + } + } } - -.tt-suggestion:hover { - cursor: pointer; - color: #fff; - background-color: #0097cf; -} - -.tt-suggestion.tt-cursor { - color: #fff; - background-color: #0097cf; - -} - -.tt-suggestion p { - margin: 0; -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 875c4bce6..990e0646d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9256,9 +9256,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001680", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", - "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==" + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==" }, "capture-exit": { "version": "2.0.0", @@ -26916,14 +26916,6 @@ "mime-types": "~2.1.24" } }, - "typeahead.js": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/typeahead.js/-/typeahead.js-0.11.1.tgz", - "integrity": "sha512-yGaLzGjVHyryZdNfrWz2NHXUwEO7hrlVmGMGCo5+6mH3nEEhcQ0Te3mK3G60uRnxfERu8twOWSU4WmwScbwhMg==", - "requires": { - "jquery": ">=1.7" - } - }, "typed-array-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", diff --git a/package.json b/package.json index fe4f8ebe0..460177d7f 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "jsonwebtoken": "^8.5.1", "loader.js": "^4.7.0", "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "log4js": "^6.9.1", "moment": "^2.30.1", "mongoose": "^5.13.22", @@ -88,7 +89,6 @@ "socket.io": "^2.5.0", "sweetalert2": "^11.12.4", "tracked-built-ins": "^3.3.0", - "typeahead.js": "^0.11.1", "underscore": "^1.13.6", "uuid": "^8.3.2", "validate.js": "^0.13.1", From 4bde6b6a2aca4a9353a1885aa5a59c50afde4b18 Mon Sep 17 00:00:00 2001 From: Irv Katz <60225276+exidy80@users.noreply.github.com> Date: Thu, 1 May 2025 13:38:02 -0400 Subject: [PATCH 04/21] fixes to typeahead and user-info --- app/components/ui/twitter-typeahead.hbs | 11 +++++-- app/components/ui/twitter-typeahead.js | 38 +++++++++++++++++++------ app/components/user-info.hbs | 4 +-- app/components/user-info.js | 21 +++++++------- app/styles/_type-ahead-twitter.scss | 8 +++++- package.json | 2 +- 6 files changed, 60 insertions(+), 24 deletions(-) diff --git a/app/components/ui/twitter-typeahead.hbs b/app/components/ui/twitter-typeahead.hbs index e51c18fcd..bba4d3ed8 100644 --- a/app/components/ui/twitter-typeahead.hbs +++ b/app/components/ui/twitter-typeahead.hbs @@ -6,12 +6,19 @@ placeholder={{this.placeholder}} value={{this.query}} {{on 'input' this.updateQuery}} + {{on 'keydown' this.handleKeydown}} /> {{#if this.hasResults}}
    - {{#each this.filteredList as |entry|}} -
  • + {{#each this.filteredList as |entry index|}} +
  • {{entry.label}}
  • {{/each}} diff --git a/app/components/ui/twitter-typeahead.js b/app/components/ui/twitter-typeahead.js index 7c48d9a7d..198c504e9 100644 --- a/app/components/ui/twitter-typeahead.js +++ b/app/components/ui/twitter-typeahead.js @@ -1,13 +1,12 @@ -// app/components/ui/typeahead.js - import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; -import { debounce } from 'lodash-es'; +import debounce from 'lodash-es/debounce'; export default class UiTypeaheadComponent extends Component { @tracked query = ''; @tracked isOpen = false; + @tracked highlightedIndex = -1; debouncedCloseDropdown = debounce(() => { this.isOpen = false; @@ -21,10 +20,6 @@ export default class UiTypeaheadComponent extends Component { return this.args.minLength ?? 1; } - get optionLabelPath() { - return this.args.optionLabelPath || 'id'; - } - get filteredList() { const dataList = this.args.dataList || []; const query = this.query.trim().toLowerCase(); @@ -52,8 +47,17 @@ export default class UiTypeaheadComponent extends Component { @action updateQuery(event) { - this.query = event.target.value; + const value = event.target.value; + this.query = value; + this.highlightedIndex = -1; this.isOpen = true; + + if ( + this.args.setSelectedValueOnChange && + typeof this.args.onSelect === 'function' + ) { + this.args.onSelect(value); + } } @action @@ -76,4 +80,22 @@ export default class UiTypeaheadComponent extends Component { handleBlur() { this.debouncedCloseDropdown(); } + + @action + handleKeydown(event) { + if (!this.hasResults) return; + + const maxIndex = this.filteredList.length - 1; + + if (event.key === 'ArrowDown') { + event.preventDefault(); + this.highlightedIndex = Math.min(this.highlightedIndex + 1, maxIndex); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0); + } else if (event.key === 'Enter' && this.highlightedIndex >= 0) { + event.preventDefault(); + this.selectItem(this.filteredList[this.highlightedIndex]); + } + } } diff --git a/app/components/user-info.hbs b/app/components/user-info.hbs index 52333ef94..5a60d3c37 100644 --- a/app/components/user-info.hbs +++ b/app/components/user-info.hbs @@ -117,10 +117,10 @@ {{#if this.isEditing}} {{#if @currentUser.isAdmin}} + {{@user.organization.name}} { if (result.value) { - this.send('createNewOrg'); + this.createNewOrg(); } }); } diff --git a/app/styles/_type-ahead-twitter.scss b/app/styles/_type-ahead-twitter.scss index 89f1350d9..0e502a114 100644 --- a/app/styles/_type-ahead-twitter.scss +++ b/app/styles/_type-ahead-twitter.scss @@ -30,11 +30,17 @@ li { padding: 0.5rem; cursor: pointer; - + margin: 0; + &:hover { color: #eee; background-color: #0097cf; } + + &.highlighted-item { + background-color: #0097cf; + color: #eee; + } } } } diff --git a/package.json b/package.json index 460177d7f..08fb210a0 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "randomcolor": "^0.6.2", "request": "^2.88.2", "selectize": "^0.12.6", - "selenium-webdriver": "^4.21.0", "sharp": "^0.29.2", "socket.io": "^2.5.0", "sweetalert2": "^11.12.4", @@ -151,6 +150,7 @@ "qunit": "^2.21.0", "qunit-dom": "^2.0.0", "sass": "^1.77.3", + "selenium-webdriver": "^4.21.0", "webpack": "^5.91.0" }, "engines": { From bc1ad2d7c65aa14b71131ca7a3f2ba169c5aff72 Mon Sep 17 00:00:00 2001 From: Irv Katz <60225276+exidy80@users.noreply.github.com> Date: Thu, 1 May 2025 14:17:55 -0400 Subject: [PATCH 05/21] fixed typeahead clients section-new and user-new/signup --- app/components/section-new.hbs | 2 +- app/components/section-new.js | 42 ++++------------------------------ app/components/user-new.hbs | 9 +++----- app/components/user-signup.js | 13 +++++------ app/services/error-handling.js | 4 ---- 5 files changed, 15 insertions(+), 55 deletions(-) diff --git a/app/components/section-new.hbs b/app/components/section-new.hbs index 8ddb9571a..3080b05d5 100644 --- a/app/components/section-new.hbs +++ b/app/components/section-new.hbs @@ -60,7 +60,7 @@ @placeholder="Teacher's username" @dataList={{@addableTeachers}} @optionLabelPath='username' - @selectedValue={{this.teacher}} + @onSelect={{this.setTeacher}} @minLength={{2}} @setSelectedValueOnChange={{true}} /> diff --git a/app/components/section-new.js b/app/components/section-new.js index 99397f2c2..92573a6d9 100644 --- a/app/components/section-new.js +++ b/app/components/section-new.js @@ -10,7 +10,6 @@ export default class SectionNewComponent extends ErrorHandlingComponent { @service router; @service('sweet-alert') alert; @tracked createRecordErrors = []; - @tracked teacher = null; @tracked leader = null; @tracked teachers = []; @tracked selectedOrganization = null; @@ -53,42 +52,6 @@ export default class SectionNewComponent extends ErrorHandlingComponent { } } - // get invalidTeacherUsername() { - // return !this.teacher; - // } - - // setTeacher: observer('teacher', function () { - // let teacher = this.teacher; - // if (!teacher) { - // if (this.organization) { - // this.set('organization', null); - // } - // return; - // } - - // if (typeof teacher === 'string') { - // let users = this.users; - // let user = users.findBy('username', teacher); - // if (!user) { - // this.set('invalidTeacherUsername', true); - // this.set('organization', null); - // return; - // } - // teacher = user; - // } - - // let organization = teacher.get('organization'); - - // if (organization) { - // this.set('organization', organization); - // } else { - // this.set('organization', this.get('currentUser.organization')); - // } - // if (this.invalidTeacherUsername) { - // this.set('invalidTeacherUsername', null); - // } - // }), - get validTeacher() { return this.teacher && !this.invalidTeacherUsername; } @@ -176,4 +139,9 @@ export default class SectionNewComponent extends ErrorHandlingComponent { @action cancel() { this.router.transitionTo('sections'); } + + @action + setTeacher(selectedTeacher) { + this.teacher = selectedTeacher; + } } diff --git a/app/components/user-new.hbs b/app/components/user-new.hbs index cbdb63512..6c1041afe 100644 --- a/app/components/user-new.hbs +++ b/app/components/user-new.hbs @@ -52,9 +52,8 @@ @@ -64,10 +63,8 @@ {{#if @currentUser.isAdmin}} {{else}} {{@currentUser.organization.name}} diff --git a/app/components/user-signup.js b/app/components/user-signup.js index cae104eb6..b0e190c6c 100644 --- a/app/components/user-signup.js +++ b/app/components/user-signup.js @@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; export default class UserSignupComponent extends ErrorHandlingComponent { - emailRegEx = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; + emailRegEx = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; usernameRegEx = /^[a-z0-9_]{3,30}$/; passwordMinLength = 10; passwordMaxLength = 72; @@ -62,11 +62,8 @@ export default class UserSignupComponent extends ErrorHandlingComponent { } validateEmail(email) { - if (!email) { - return false; - } - var emailPattern = new RegExp(this.emailRegEx); - return emailPattern.test(email); + if (!email) return false; + return this.emailRegEx.test(email.trim()); } validatePassword(password) { @@ -123,7 +120,8 @@ export default class UserSignupComponent extends ErrorHandlingComponent { this.password = password; } } - @action emailValidate(email) { + @action emailValidate(event) { + const email = event.target.value; let isValid = this.validateEmail(email); if (isValid) { this.emailError = null; @@ -132,6 +130,7 @@ export default class UserSignupComponent extends ErrorHandlingComponent { this.emailError = this.emailErrors.invalid; } } + @action resetErrors() { const errors = [ 'missingCredentials', diff --git a/app/services/error-handling.js b/app/services/error-handling.js index b5fab5ae2..8e5fab69c 100644 --- a/app/services/error-handling.js +++ b/app/services/error-handling.js @@ -6,14 +6,10 @@ import { tracked } from '@glimmer/tracking'; * plus optional record rollback and SweetAlert toasts. */ export default class ErrorHandlingService extends Service { - /** - * The SweetAlert service for showing error toasts, etc. - */ @service('sweet-alert') alert; /** * Store of all errors, keyed by a property name (e.g., "problemLoadErrors"). - * Using a TrackedObject so that updates remain reactive in templates. */ @tracked errors = {}; From f37bb19d0ff71e3fe8b2211baea8e3e5faa33752 Mon Sep 17 00:00:00 2001 From: Irv Katz <60225276+exidy80@users.noreply.github.com> Date: Mon, 5 May 2025 15:21:58 -0400 Subject: [PATCH 06/21] fixes elated to answer-new and assignments. Also some services. --- UPGRADE_NOTES.md | 22 ++ app/components/answer-new.hbs | 53 ++--- app/components/answer-new.js | 116 +++++----- app/components/assignment-info-student.hbs | 33 ++- app/components/assignment-new.js | 8 +- app/components/image-upload.js | 2 +- app/helpers/not-equal.js | 5 + app/models/folder.js | 8 +- app/routes/assignments/assignment.js | 7 +- app/services/string-similarity.js | 27 ++- .../assignment-info-student-test.js | 202 ++++++++++++++++++ 11 files changed, 359 insertions(+), 124 deletions(-) create mode 100644 app/helpers/not-equal.js create mode 100644 tests/integration/components/assignment-info-student-test.js diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md index 6ba0556ba..13d9762c3 100644 --- a/UPGRADE_NOTES.md +++ b/UPGRADE_NOTES.md @@ -323,3 +323,25 @@ The following components, included above, are actually wrappers for third-party - problems/problem//assignment Previously, edit and assignment were handled by flags in the db inside of a problem. When we transitioned to the problem, we first set the correct flag so that would render correctly. With this change, we don't use the db for local application state, which is poor practice. + +## Quill upgrades + +I upgraded the loading and use of the Quill (enhanced text editing) package: + +- Quill is now loaded as any other third party package, rather than via the vendor folder and manual install. +- The Ember component, quill-container, has been upgraded to modern Ember (v4.5 at least). +- The quill-container component has been moved to the components/ui folder. +- In all quill-container clients, the reference has been updated (<>) +- TODO: in response-submission-view.hbs, the use of quill-container has always had an incorrect signature. I need to figure out what would be the correct signature should be. + +## Validate.js + +This package's loading has also been upgrading to the modern approach. TODO -- consider whether a custom utility function should replace using a 3rd party package. + +## Twitter typeahead + +This package has been removed from the system in favor of a simpler, custom component. The new component is contained within the file twitter-typeahead.js/hbs just to simplify references throughout the system. I tested all clients of the new component and they appear to work. Still need to test the usage in the signup-google component (TODO). Note that getting typeahead to work in some components (section-new, user-info, and user-new) involved upgrades to those components because of various old-style Ember idioms, such as the use of two-way binding. + +## lodash-es + +loadash-es is now loaded. I did this because some of the regular lodash subpackages (e.g., lodash/isEqual) are being deprecated. Thus, the correct thing to do now is to use lodash-es/isEqual, for example. Eventually, I should change all uses of lodash to lodash-es. diff --git a/app/components/answer-new.hbs b/app/components/answer-new.hbs index 79e7f7a6a..fba569b4e 100644 --- a/app/components/answer-new.hbs +++ b/app/components/answer-new.hbs @@ -16,17 +16,18 @@ *