Permalink
Browse files

Merge pull request #1378 from sproutcore/team/nicolasbadia/select_ext

Introduces a few experimental views based on SC.SelectView
  • Loading branch information...
2 parents 753b50c + a4782f7 commit fdeb391423db2862c73f4a036e3f4116be342c7c @nicolasbadia nicolasbadia committed on GitHub Jul 21, 2016
View
33 frameworks/experimental/frameworks/select_ext/mixins/item_filter.js
@@ -0,0 +1,33 @@
+
+SC.ItemFilter = {
+
+
+ filterItems: function(value) {
+ this._searchedValue = value;
+ this.invokeOnceLater('doFilterItems', 250);
+ },
+
+ doFilterItems: function() {
+ if (!this._initialItems) this._initialItems = this.get('items');
+
+ var value = this._searchedValue,
+ itemTitleKey = this.get('itemSearchKey') || this.get('itemTitleKey'),
+ items = this.searchItems(this._initialItems, value, itemTitleKey);
+
+ this.set('items', items);
+ },
+
+ searchItems: function(items, value, key) {
+ if (value) {
+ items = items.filter(function(item) {
+ var itemValue = key ? SC.get(item, key) : item;
+ return SC.typeOf(itemValue) === SC.T_STRING ? itemValue.search(new RegExp(value, "i")) !== -1 : false;
+ });
+ }
+
+ return items;
+ },
+
+};
+
+
View
34 frameworks/experimental/frameworks/select_ext/resources/multi_select.css
@@ -0,0 +1,34 @@
+$theme.sc-multi-select-view {
+ background-color: #FFF;
+ border: 1px solid #CCC;
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+ border-radius: 2px;
+
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+$theme.sc-multi-select-item-view {
+ display: none;
+
+ &.selected {
+ display: inline-block;
+ border: 1px #AAA solid;
+ padding: 1px 4px;
+ margin: 1px;
+ position: relative;
+ width: auto;
+ background-color: #DDD;
+ border-radius: 3px;
+
+ i {
+ cursor: pointer;
+ color: #333;
+ margin-left: 3px;
+ }
+ }
+}
View
4 frameworks/experimental/frameworks/select_ext/sc_config
@@ -0,0 +1,4 @@
+BT.addFramework(BT.Framework.extend({
+ ref: "sproutcore:experimental:select_ext",
+ path: dirname()
+}));
View
89 frameworks/experimental/frameworks/select_ext/views/combo_box.js
@@ -0,0 +1,89 @@
+
+sc_require("mixins/item_filter");
+
+
+SC.ComboBoxView = SC.View.extend(SC.ItemFilter, {
+
+ value: null,
+
+ selectedMenuItem: null,
+
+ textFieldView: SC.TextFieldView,
+
+ createChildViews: function() {
+ var that = this,
+ view;
+
+ view = that.createChildView(this.get('textFieldView'), {
+ isEnabledBinding: SC.Binding.from('isEnabled', this).oneWay(),
+ valueBinding: SC.Binding.from('value', this),
+ hintBinding: SC.Binding.from('hint', this).oneWay(),
+ isTextArea: this.get('isTextArea'),
+ maxLength: 5096,
+
+ valueDidChange: function() {
+ if (that._menu) {
+ that._menu.remove();
+ }
+
+ var value = this.get('value');
+
+ if (value !== that._lastValue) {
+ that._lastValue = value;
+ that.filterItems(value);
+ }
+ }.observes('value'),
+ });
+
+ this.set('textFieldView', view);
+ this.set('childViews', [view]);
+ },
+
+
+ didSelectItemDelegate: function() {
+ var item = this.get('selectedMenuItem'),
+ value = SC.get(item, this.get('itemValueKey'));
+
+ this._lastValue = value;
+ this.set('value', value);
+ }.observes('selectedMenuItem'),
+
+
+
+
+ popupMenu: function() {
+ var that = this,
+ layer = this.get('layer'),
+ menu = this._menu;
+
+ if (!menu) {
+ var menu = SC.AutoResizingMenuPane.create({
+ preferMatrix: [1, 1, SC.POSITION_BOTTOM],
+
+ acceptsKeyPane: false,
+
+ itemsBinding: SC.Binding.from('items', that).oneWay(),
+
+ action: function(rootMenu) {
+ var selectedItem = rootMenu.get('selectedItem');
+ that.set('selectedMenuItem', selectedItem);
+ },
+
+ });
+
+ menu.menuItemKeys.forEach(function(menuItemKey) {
+ var itemKey = that.get(menuItemKey);
+ if (itemKey) menu.set(menuItemKey, itemKey);
+ });
+
+ this._menu = menu;
+ }
+
+ this.invokeLast(function() {
+ menu.popup(layer);
+ });
+ },
+
+
+});
+
View
360 frameworks/experimental/frameworks/select_ext/views/multi_select_view.js
@@ -0,0 +1,360 @@
+// an attempt to 'replicate' http://harvesthq.github.io/chosen/ in SC
+//
+// The current approach is to try to marry collection view and the new select view / popupbutton view
+// What is displayed is the selected pieces, and giving the view focus (by clicking for example) will
+// open the popup.
+//
+
+
+sc_require('views/collection');
+
+SC.MultiSelectView = SC.CollectionView.extend({
+
+ classNames: ['sc-multi-select-view'],
+
+ selectOnMouseDown: NO,
+
+ init: function () {
+ sc_super();
+
+ this._currentPopup = null;
+ this.invokeOnce('schedulePopupSetupIfNeeded');
+ },
+
+ // renderDelegateName: 'multiSelectViewRenderDelegate',
+ //
+ exampleView: SC.View.extend({
+ useStaticLayout: true,
+ classNames: ['sc-multi-select-item-view'],
+ displayProperties: ['isSelected'],
+ contentIndex: null,
+ escapeHTML: true,
+
+ /** @private
+ Determines if the event occurred inside an element with the specified
+ classname or not.
+ */
+ _isInsideElementWithClassName: function (className, evt) {
+ var layer = this.get('layer');
+ if (!layer) return NO; // no layer yet -- nothing to do
+
+ var el = SC.$(evt.target);
+ var ret = NO;
+ while (!ret && el.length > 0 && (el[0] !== layer)) {
+ if (el.hasClass(className)) ret = YES;
+ el = el.parent();
+ }
+ el = layer = null; //avoid memory leaks
+ return ret;
+ },
+
+ mouseDown: function (evt) {
+ // Fast path, reject secondary clicks.
+ if (evt.which && evt.which !== 1) return false;
+
+ // if content is not editable, then always let collection view handle the
+ // event.
+ if (this._isInsideElementWithClassName('fa-remove', evt)) {
+ this._isMouseDownOnRemove = YES;
+ return YES;
+ }
+
+ return NO; // let the collection view handle this event
+ },
+
+ mouseUp: function (evt) {
+ if (this._isMouseDownOnRemove) {
+ if (this._isInsideElementWithClassName('fa-remove', evt)) {
+ // remove the item from selection
+ var del = this.displayDelegate;
+ del.deselect(this.get('contentIndex'));
+ return YES;
+ }
+ this._isMouseDownOnRemove = NO;
+ }
+ return NO;
+ },
+
+ render: function (context, firstTime) {
+ // only render something when selected
+ var ckv = this.get('contentValueKey') || 'title';
+ var del = this.displayDelegate;
+ // console.log('render in exampleView: displayDelegate ', del);
+ // window.DDEL = del;
+ var labelKey = this.getDelegateProperty('contentValueKey', del);
+ // console.log('labelKey: ', labelKey);
+ var content = this.get('content');
+ var value = (labelKey && content) ? (content.get ? content.get(labelKey) : content[labelKey]) : content;
+ if (this.get('escapeHTML')) value = SC.RenderContext.escapeHTML(value);
+ // also measure (somehow) the width
+ if (this.get('isSelected') && value) {
+ context.addClass('selected');
+ context.push(value);
+ context.push('<i class="fa fa-remove"></i>');
+ }
+ else {
+ context.removeClass('selected');
+ context.push("");
+ }
+ }
+ }),
+
+ _computedLayoutsForSelection: function () {
+ console.log('recalculation _computedLayoutsForSelection');
+ var sel = this.get('selection');
+ var frame = this.get('frame');
+ var content = this.get('content');
+ var maxW = frame.width;
+ var height = frame.height;
+ if (!sel) return null;
+ if (!content) return null;
+ var ckv = this.get('contentValueKey');
+ var left = 0;
+ var top = 0;
+ return sel.map(function (v) {
+ var value = SC.get(content.objectAt(v), ckv);
+ var width = SC.metricsForString(value, 'div', ['fa-remove', 'sc-multi-select-item-view']).width * 2;
+ var ret = {
+ left: left,
+ top: top,
+ width: width
+ };
+ left += width;
+ if (left > maxW) {
+ left = 0;
+ top += frame.height; // mmm?
+ }
+ });
+ }.property('selection').cacheable(),
+
+ /** @private */
+
+ // perhaps something completely different should be done here, ie
+ // a precalculation of the selected items, which this layoutForContentIndex
+ // just has to look up. Reason is that otherwise it should constantly look up the others
+ layoutForContentIndex: function (contentIndex) {
+ // var rowHeight = this.get('rowHeight') || 32,
+ // frameWidth = this.get('frame').width,
+ // itemsPerRow = this.get('itemsPerRow') || 4,
+ // columnWidth = Math.floor(frameWidth / itemsPerRow),
+ // row = Math.floor(contentIndex / itemsPerRow),
+ // col = contentIndex - (itemsPerRow * row);
+
+ // var content = this.get('content');
+ // var sel = this.get('selection');
+ // var width = 0;
+ // // do something with the index in the selection to get the positioning done...
+
+ // if (content && content.objectAt) {
+ // var item = content.objectAt(contentIndex);
+ // var labelKey = this.get('contentValueKey');
+ // var value = SC.get(item, labelKey);
+ // width = SC.metricsForString(value, 'div', ['fa-remove', 'sc-multi-select-item-view']).width * 2;
+ // }
+
+
+
+ var selectedIndexes = this.get('selectedIndexes');
+ if (!selectedIndexes) return {};
+
+ console.log('selected Indexes: ', selectedIndexes, 'this index: ', contentIndex);
+
+ var selectionIndex = selectedIndexes.indexOf(contentIndex);
+ if (selectionIndex === -1) return {};
+
+ // If the frame is not ready, then just return an empty layout.
+ // Otherwise, NaN will be entered into layout values.
+ // if (frameWidth === 0 || itemsPerRow === 0) {
+ // return {};
+ // }
+
+
+ var layout = this.get('_computedLayoutsForSelection')[selectionIndex];
+ return layout;
+ // return {
+ // left: 'auto',
+ // top: 0,
+ // // height: rowHeight,
+ // width: width
+ // };
+ },
+
+ computeLayout: function () {
+ sc_super();
+ // should calculate the layout of the view.
+ },
+
+ acceptsFirstResponder: true,
+
+ popup: SC.AutoResizingMenuPane.extend({
+ exampleView: SC.AutoResizingMenuItemView.extend({
+ isEnabled: function () {
+ if (window.isEnabledCount === undefined) window.isEnabledCount = 0;
+ console.log('isEnabled is being looked up', window.isEnabledCount);
+ // debugger;
+ window.isEnabledCount += 1;
+ // look up whether the current item is selected
+ var sel = this.getPath('parentMenu.rootMenu.multiSelectView.selection');
+ return !(sel && sel.contains(this.getPath('content.item')));
+ }.property('parentMenu.rootMenu.multiSelectView.selection'),
+
+ render: function (context, firstTime) {
+ sc_super();
+ if (this.get('isEnabled')) {
+ context.removeClass('disabled');
+ }
+ else context.addClass('disabled');
+ }
+ })
+
+ }),
+
+ shouldLoadInBackground: YES,
+
+ schedulePopupSetupIfNeeded: function () {
+ var popup = this.get('popup');
+ if (popup && popup.isClass && this.get('shouldLoadInBackground')) {
+ SC.backgroundTaskQueue.push(SC.MultiSelectView.InstantiatePopupTask.create({ multiSelectView: this }));
+ }
+ },
+
+ setupPopup: function () {
+ var popup = this.get('popup');
+ if (popup === this._currentPopup) return;
+ if (this._currentPopup) {
+ this.isActiveBinding.disconnect();
+ this.allItemsBinding.disconnect();
+ this._contentItemKeyBinding.disconnect();
+ this._currentPopup.destroy();
+ this._currentPopup = null;
+ }
+
+ if (popup && popup.isClass) {
+ popup = this.createPopup(popup);
+ window.POPUP = popup;
+ }
+ var me = this;
+ this._currentPopup = popup;
+ this.set('popup', popup);
+ popup.set('minimumMenuWidth', this.get('frame').width);
+ popup.set('width', this.get('frame').width);
+ this.isActiveBinding = this.bind('isActive', popup, 'isVisibileInWindow');
+ this.allItemsBinding = this.bind('allItems', popup, 'items');
+ this._selectedValueBinding = this.bind('_selectedValue', popup, 'selectedItem');
+ },
+
+ _selectedValueDidChange: function () {
+ var val = this.getPath('_selectedValue.value');
+ if (val === undefined) return;
+
+ this.select(val, true); // always extend selection
+
+ if (this.popup) {
+ this.popup.notifyPropertyChange('displayItems');
+ }
+ }.observes('_selectedValue'),
+
+ allItems: function () {
+ console.log('allItmes');
+ var sel = this.get('selection');
+ var titleKey = this.get('contentValueKey');
+ var me = this;
+ var count = 0;
+ return this.get('content').map(function (item, index) {
+ return SC.Object.create({
+ title: SC.get(item, titleKey),
+ value: index,
+ item: item
+ });
+ });
+ }.property().cacheable(),
+
+ createPopup: function (popup) {
+ return popup.create({
+ multiSelectView: this
+ });
+ },
+
+ showPopup: function () {
+ this.setupPopup();
+
+ this.invokeLast('_showPopup');
+ },
+
+ hidePopup: function () {
+ var popup = this.get('popup');
+ if (popup && !popup.isClass) {
+ popup.remove();
+ }
+ },
+
+ popupLeftOffset: SC.propertyFromRenderDelegate('menuLeftOffset', 0),
+ popupTopOffset: SC.propertyFromRenderDelegate('menuTopOffset', 0),
+
+ /**
+ The prefer matrix for menu positioning. It is calculated so that the selected
+ menu item is positioned directly below the MultiSelectView.
+ @property
+ @type Array
+ @private
+ */
+ popupPreferMatrix: function() {
+ var popup = this.get('popup'),
+ frame = this.get('frame'),
+ height = frame.height,
+ leftPosition = this.get('popupLeftOffset'),
+ topPosition = this.get('popupTopOffset');
+
+ if (!popup) {
+ return [leftPosition, topPosition, 0];
+ }
+
+ var idx = this.get('_selectedItemIndex'), itemViews = popup.get('menuItemViews');
+ if (idx > -1) {
+ var layout = itemViews[idx].get('layout');
+ return [leftPosition, topPosition - layout.top + (layout.height/2), 0];
+ }
+
+ return [leftPosition, topPosition + height, 2];
+
+ }.property('value', 'menu').cacheable(),
+
+ _showPopup: function () {
+ var popup = this.get('popup');
+ popup.popup(this, this.get('popupPreferMatrix'));
+ },
+
+
+ //when mouseDown is over an empty area in the view, ie not one of the already selected
+ // options, it should show the popup.
+ mouseDown: function (evt) {
+ // sc_super();
+ // if (!this.get('isEnabled')) return YES;
+ // this.set('_mouseDown', YES);
+ // // decide whether it needs popups
+ // debugger;
+ this.showPopup();
+ this.becomeFirstResponder();
+ return YES;
+ },
+
+ mouseUp: function (evt) {
+ return YES;
+ },
+
+ _popupIsLoaded: NO,
+
+ isActive: NO,
+
+
+});
+
+SC.MultiSelectView.InstantiatePopupTask = SC.Task.extend({
+ multiSelectView: null,
+
+ run: function (queue) {
+ this.multiSelectView.setupPopup();
+ }
+});
+
+
View
82 frameworks/experimental/frameworks/select_ext/views/select_search_view.js
@@ -0,0 +1,82 @@
+
+sc_require("mixins/item_filter");
+
+
+SC.SelectSearchView = SC.SelectView.extend(SC.ItemFilter, {
+
+ menuPreferMatrix: [0, -24, SC.POSITION_BOTTOM],
+
+
+ searchedValue: null,
+
+
+ searchedValueDidChange: function() {
+ this.filterItems(this.get('searchedValue'));
+ }.observes('searchedValue'),
+
+
+ menu: SC.AutoResizingMenuPane.extend(SC.SelectViewMenu, {
+ // Prevent the text field from loosing the focus
+ // UPDATE: Not working and still buggy
+ //exampleView: SC.AutoResizingMenuItemView.extend({
+ // acceptsKeyPane: false,
+ //}),
+
+ isKeyPane: true,
+
+ didAppendToDocument: function () {
+ sc_super();
+ this._textFieldView.beginEditing();
+ },
+
+ createChildViews: function () {
+ var textField, scroll, menuView;
+
+ menuView = this._menuView = SC.View.create({
+ layout: { height: 0 }
+ });
+
+ textField = this._textFieldView = this.createChildView(SC.TextFieldView, {
+ layout: { top: 1, right: 1, left: 1, height: 24 },
+ controlSize: this.get('controlSize'),
+
+ leftAccessoryView: SC.View.extend({
+ layout: { top: 3, left: 4, height: 20, width: 20 },
+ classNames: ['fa fa-search'],
+ })
+ });
+
+ textField.bind('value', this.get('selectView'), 'searchedValue');
+
+ scroll = this._menuScrollView = this.createChildView(SC.MenuScrollView, {
+ layout: { top: 25 },
+ controlSize: this.get('controlSize'),
+ contentView: menuView
+ });
+
+ this.childViews = [textField, scroll];
+
+ return this;
+ },
+
+
+ _updateMenuWidth: function() {
+ var menuItemViews = this.get('menuItemViews');
+ if (!menuItemViews) return;
+
+ var len = menuItemViews.length, idx, view,
+ width = this.get('minimumMenuWidth');
+
+ for (idx = 0; idx < len; idx++) {
+ view = menuItemViews[idx];
+ width = Math.max(width, view.get('measuredSize').width + this.get('menuWidthPadding'));
+ }
+
+ this.adjust({ 'width': width, height: this.get('menuHeight')+25 });
+ this.positionPane();
+ },
+
+ }),
+
+});
+

0 comments on commit fdeb391

Please sign in to comment.