{"id":16655547,"url":"https://github.com/aaronrenner/ember-popup-menu","last_synced_at":"2025-10-08T19:21:31.027Z","repository":{"id":27402193,"uuid":"30878690","full_name":"aaronrenner/ember-popup-menu","owner":"aaronrenner","description":null,"archived":false,"fork":false,"pushed_at":"2015-02-16T18:34:47.000Z","size":375,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-11T04:45:49.696Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"surbhioberoi/github-widget","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aaronrenner.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-02-16T17:11:44.000Z","updated_at":"2020-09-08T17:48:57.000Z","dependencies_parsed_at":"2022-08-07T12:16:34.307Z","dependency_job_id":null,"html_url":"https://github.com/aaronrenner/ember-popup-menu","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aaronrenner/ember-popup-menu","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronrenner%2Fember-popup-menu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronrenner%2Fember-popup-menu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronrenner%2Fember-popup-menu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronrenner%2Fember-popup-menu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aaronrenner","download_url":"https://codeload.github.com/aaronrenner/ember-popup-menu/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aaronrenner%2Fember-popup-menu/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279000701,"owners_count":26082805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-12T09:53:21.412Z","updated_at":"2025-10-08T19:21:31.012Z","avatar_url":"https://github.com/aaronrenner.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ember {{popup-menu}}\n\nThis popup-menu provides a pluggable interface for dealing with popups around your site. It has an inteface for registering constraint behaviors and animations.\n\nFor use of the popup-menu as a tooltip, the following handlebars will do the trick:\n\n```handlebars\n\u003cspan id=\"help-me\" class=\"icon-help\"\u003e\u003c/span\u003e\n{{#popup-menu for=\"help-me\" on=\"hover\"}}\n  Hey there!\n{{/popup-menu}}\n```\n\n## Installation\n\n* `npm install --save-dev ember-popup-menu`\n* `ember g ember-popup-menu`\n\n## Recipes\n\nTooltips:\n```javascript\nimport PopupMenu from \"ember-popup-menu/components/popup\";\n\nvar ToolTip = PopupMenu.extend({\n  classNames: ['tool-tip'],\n  layoutName: 'components/popup-menu',\n  on: ['hover', 'focus'],\n  flow: 'popup'\n});\n\nexport default ToolTip;\n```\n\n```handlebars\n\u003cspan id=\"help-me\" class=\"icon-help\"\u003e\u003c/span\u003e\n{{#tool-tip for=\"help-me\"}}\n  Hey there!\n{{/tool-tip}}\n```\n\nDropdown menu:\n```javascript\nimport PopupMenu from \"ember-popup-menu/components/popup\";\n\nvar DropDown = PopupMenu.extend({\n  classNames: ['drop-down'],\n  layoutName: 'components/popup-menu',\n  on: ['hover', 'focus', 'hold'],\n  flow: 'dropdown'\n});\n\nexport default DropDown;\n```\n\n```handlebars\n\u003cdiv id=\"current-user\"\u003eMe\u003c/div\u003e\n{{#drop-down for=\"current-user\"}}\n  \u003cul\u003e\n    \u003cli\u003eSettings\u003c/li\u003e\n    \u003cli\u003eBilling\u003c/li\u003e\n  \u003c/ul\u003e\n{{/drop-down}}\n```\n\n## Writing your own components using {{popup-menu}}\n\nThe {{popup-menu}} component is designed to be used with other components. It provides a programatic API for adding customized targets, and a set of utilities that allow for an easier and more consistent development experience when authoring these addons.\n\nLet's go through the steps of authoring a component that uses a {{popup-menu}} by making a {{date-picker}} widget. Some of the implementation details will be ignored to make this tutorial clearer to follow.\n\nFirst, let's bootstrap the addon:\n\n```bash\n$ ember addon my-date-picker\n```\n\nAfter this we'll add `ember-popup-menu` and `ember-moment` as a dependencies (*not* a development dependency):\n\n```bash\n$ cd my-date-picker\n$ npm install --save ember-popup-menu\n$ ember g ember-popup-menu\n$ npm install --save ember-moment\n$ ember g ember-moment\n```\n\nNow, we're ready to start authoring the addon. Let's first start by creating the component javascript file.\n\n```bash\n$ mkdir addon/components\n$ touch addon/components/date-picker.js\n```\n\nUsing the editor of your choice, add the following bootstrap code to get started:\n\n```javascript\nimport Ember from \"ember\";\n\nvar DatePicker = Ember.Component.extend({\n  classNames: ['date-picker']\n});\n\nexport default DatePicker;\n```\n\nLet's define our public API first. This is what you will use to interface with the component in handlebars:\n\n```javascript\nimport Ember from \"ember\";\n\nvar DatePicker = Ember.Component.extend({\n  value: null,\n  icon: null\n});\n\nexport default DatePicker;\n```\n\n`value` is the date that is picked and `icon` is an icon used to display a calendar icon.\n\nWe're going to make the date picker a combination of a text field and a configurable icon, so let's start hooking them up so the popup-menu knows what will trigger events:\n\n```javascript\nimport Ember from \"ember\";\nimport nearestChild from \"ember-popup-menu/computed/nearest-child\";\n\nvar get = Ember.get;\n\nvar DatePicker = Ember.Component.extend({\n  classNames: ['date-picker'],\n\n  value: null,\n  icon: null,\n  \n  popup: nearestChild('popup-menu'),\n  \n  attachTargets: function () {\n    var popup = get(this, 'popup');\n    var icon = get(this, 'icon');\n\n    popup.addTarget(icon, {\n      on: \"click\"\n    });\n  }.on('didInsertElement')\n});\n\nexport default DatePicker;\n```\n\nLet's walk through the code.\n\nFirst, we imported `nearestChild`. This is a computed property that returns the nearest child of a given type. We then use this property to get the popup-menu.\n\nThen we add the icon as a target for the popup menu that will toggle the menu when clicked.\n\nFor the next step, let's start showing the popup live and doing some iterative development. To do this, we'll need to start fiddling with the app directory.\n\nCreate the `components` and `templates/components` directories under `app` at the root of your addon's project.\n\nThe first thing to do is expose the component as a public interface by making a file called `date-picker.js` under `components`:\n\n```javascript\nimport DatePicker from \"my-date-picker/components/date-picker\";\nexport default DatePicker;\n```\n\nThis simply exposes the date picker as a component consumable by the host application.\n\nNext, let's add a handlebars template for the date picker, under `templates/components/date-picker.hbs`:\n\n```handlebars\n{{input type=text value=displayValue}}\u003cspan class=\"icon-calendar\" {{bind-attr id=icon}}\u003eOpen\u003c/span\u003e\n{{#popup-menu flow=\"dropdown\" will-change=\"month\" month=month}}\n  \u003cheader\u003e\n    \u003ca class=\"previous-month\" {{action \"previousMonth\"}}\u003e\u0026lt;\u003c/a\u003e\n    \u003cdiv class=\"month\"\u003e{{moment firstOfMonth \"MMMM\"}}\u003c/div\u003e\n    \u003ca class=\"next-month\" {{action \"nextMonth\"}}\u003e\u0026gt;\u003c/a\u003e\n  \u003c/header\u003e\n  {{calendar-month month=firstOfMonth}}\n{{/popup-menu}}\n```\n\nWith the template we created, we've solidified a few requirements for the component. Let's go back to `date-picker.js` in the addon directory and suss these out.\n\nFirst, let's automatically generate an ID for the icon. This way, the popup-menu has a unique identifier for triggering on. While we're at it, let's implement the details around `month`.\n\n```javascript\nimport Ember from \"ember\";\nimport nearestChild from \"ember-popup-menu/computed/nearest-child\";\n\nvar generateGuid = Ember.generateGuid;\n\nvar get = Ember.get;\n\nvar DatePicker = Ember.Component.extend({\n  classNames: ['date-picker'],\n  \n  value: null,\n  icon: function () {\n    return generateGuid();\n  }.property(),\n  \n  popup: nearestChild('popup-menu'),\n  \n  attachTargets: function () {\n    var popup = get(this, 'popup');\n    var icon = get(this, 'icon');\n\n    popup.addTarget(icon, {\n      on: \"click\"  \n    });\n  }.on('didInsertElement'),\n  \n  actions: {\n    previousMonth: function () {\n      var previousMonth = get(this, 'firstOfMonth').clone().subtract(1, 'month');\n      set(this, 'month', previousMonth.month());\n      set(this, 'year', previousMonth.year());\n    },\n    \n    nextMonth: function () {\n      var nextMonth = get(this, 'firstOfMonth').clone().add(1, 'month');\n      set(this, 'month', nextMonth.month());\n      set(this, 'year', nextMonth.year());\n    }\n  },\n  \n  month: null,\n  year: null,\n  \n  firstOfMonth: function () {\n    return moment({ year: get(this, 'year'), month: get(this, 'month') });\n  }.property('year', 'month')\n\n});\n\nexport default DatePicker;\n```\n\nAs a default, let's make month be the current month *or* the month of the selected value:\n\n```javascript\nimport Ember from \"ember\";\nimport moment from 'moment';\nimport nearestChild from \"ember-popup-menu/computed/nearest-child\";\n\nvar generateGuid = Ember.generateGuid;\n\nvar get = Ember.get;\n\nvar reads = Ember.computed.reads;\n\nvar DatePicker = Ember.Component.extend({\n  classNames: ['date-picker'],\n  \n  value: null,\n  icon: function () {\n    return generateGuid();\n  }.property(),\n  \n  popup: nearestChild('popup-menu'),\n  \n  attachTargets: function () {\n    var popup = get(this, 'popup');\n    var icon = get(this, 'icon');\n\n    popup.addTarget(icon, {\n      on: \"click\"\n    });\n  }.on('didInsertElement'),\n  \n  actions: {\n    previousMonth: function () {\n      var previousMonth = get(this, 'firstOfMonth').clone().subtract(1, 'month');\n      set(this, 'month', previousMonth.month());\n      set(this, 'year', previousMonth.year());\n    },\n    \n    nextMonth: function () {\n      var nextMonth = get(this, 'firstOfMonth').clone().add(1, 'month');\n      set(this, 'month', nextMonth.month());\n      set(this, 'year', nextMonth.year());\n    }\n  },\n  \n  month: reads('currentMonth'),\n  year: reads('currentYear'),\n  \n  firstOfMonth: function () {\n    return moment({ year: get(this, 'year'), month: get(this, 'month') });\n  }.property('year', 'month'),\n  \n  currentMonth: function () {\n    return get(this, 'value') ?\n           get(this, 'value').getMonth() :\n           new Date().getMonth();\n  }.property(),\n  \n  currentYear: function () {\n    return get(this, 'value') ?\n           get(this, 'value').getFullYear() :\n           new Date().getFullYear();\n  }.property(),\n  \n  displayValue: function () {\n    var value = get(this, 'value');\n    return value ? moment(value).format(\"MM/DD/YYYY\") : null;\n  }.property('value')\n});\n\nexport default DatePicker;\n```\n\nWith this much, we should be able to rotate through a list of months in the calendar year. Let's test this by commenting out the `{{calendar-month}}` component:\n\n```handlebars\n{{input type=text value=displayValue}}\u003cspan class=\"icon-calendar\" {{bind-attr id=icon}}\u003eOpen\u003c/span\u003e\n{{#popup-menu flow=\"dropdown\" will-change=\"month\" month=month}}\n  \u003cheader\u003e\n    \u003ca class=\"previous-month\" {{action \"previousMonth\"}}\u003e\u0026lt;\u003c/a\u003e\n    \u003cdiv class=\"month\"\u003e{{moment firstOfMonth \"MMMM\"}}\u003c/div\u003e\n    \u003ca class=\"next-month\" {{action \"nextMonth\"}}\u003e\u0026gt;\u003c/a\u003e\n  \u003c/header\u003e\n  {{!calendar-month month=firstOfMonth}}\n{{/popup-menu}}\n```\n\nNow on to the next step! Let's implement the calendar-month component. In `calendar-month.js` in your addon, let's add code to come up with the days of the week and weeks in the given month.\n\n```javascript\nimport Ember from \"ember\";\nimport moment from \"moment\";\n\nvar get = Ember.get;\n\nvar CalendarMonth = Ember.Component.extend({\n  classNames: ['calendar-month'],\n  tagName: \"table\",\n  \n  dayNames: function () {\n    var firstWeek = get(this, 'weeks.firstObject');\n    return firstWeek.map(function (day) {\n      return moment(day).format(\"ddd\");\n    });\n  }.property('weeks'),\n  \n  weeks: function () {\n    var month = get(this, 'month');\n    var day = month.clone().startOf('week');\n    var weeks = [];\n    var week = [];\n    for (var iDay = 0; iDay \u003c 7; iDay++) {\n      week.push(day.clone().toDate());\n      day.add(1, 'day');\n    }\n    weeks.push(week);\n\n    while (day.month() === month.month()) {\n      week = [];\n      for (iDay = 0; iDay \u003c 7; iDay++) {\n        week.push(day.clone().toDate());\n        day.add(1, 'day');\n      }\n      weeks.push(week);\n    }\n    return weeks;\n  }.property('month')\n});\n\nexport default CalendarMonth;\n```\n\nAnd now let's add the template for that. First, expose the component in the app:\n\n```javascript\nimport CalendarMonth from \"my-date-picker/components/calendar-month\";\nexport default CalendarMonth;\n```\n\nAnd then add the template for it:\n\n```handlebars\n\u003cthead\u003e\n  \u003ctr\u003e\n    {{#each dayOfWeek in dayNames}}\n      \u003cth\u003e{{dayOfWeek}}\u003c/th\u003e\n    {{/each}}\n  \u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n  {{#each week in weeks}}\n    \u003ctr\u003e\n      {{#each day in week}}\n        {{calendar-day value=day month=month}}\n      {{/each}}\n    \u003c/tr\u003e\n  {{/each}}\n\u003c/tbody\u003e\n```\n\nHmm. Looks like we have yet another component to write! Let's finish off with that one, and then pop the stack all the way back to finish off the component.\n\n```javascript\nimport Ember from \"ember\";\nimport moment from \"moment\";\nimport nearestParent from \"ember-popup-menu/computed/nearest-parent\";\n\nvar get = Ember.get;\n\nvar reads = Ember.computed.reads;\n\nvar CalendarDay = Ember.Component.extend({\n  classNames: ['calendar-day'],\n\n  tagName: \"td\",\n  classNameBindings: ['isSelected:selected', 'isToday', 'isDisabled:disabled'],\n  \n  datePicker: nearestParent('date-picker'),\n  selection: reads('datePicker.value'),\n  \n  isToday: function () {\n    return moment(get(this, 'value')).isSame(new Date(), 'day');\n  }.property('value'),\n  \n  isSelected: function () {\n    return moment(get(this, 'value')).isSame(get(this, 'selection'), 'day');\n  }.property('value', 'selection'),\n  \n  isDisabled: function () {\n    return !moment(get(this, 'value')).isSame(get(this, 'month'), 'month');\n  }.property('value', 'month'),\n  \n  click: function () {\n    if (get(this, 'isDisabled')) { return; }\n    get(this, 'datePicker').send('selectDate', get(this, 'value'));\n  }\n});\n\nexport default CalendarDay;\n```\n\n```handlebars\n\u003cspan\u003e{{moment value \"D\"}}\u003c/span\u003e\n```\n\nNow let's pop our stack and finish by writing a handler for `selectDate` in `date-picker.js`:\n\n```javascript\nimport Ember from \"ember\";\nimport moment from 'moment';\nimport nearestChild from \"ember-popup-menu/computed/nearest-child\";\n\nvar generateGuid = Ember.generateGuid;\n\nvar get = Ember.get;\nvar set = Ember.set;\n\nvar reads = Ember.computed.reads;\n\nvar DatePicker = Ember.Component.extend({\n  classNames: ['date-picker'],\n  value: null,\n  icon: function () {\n    return generateGuid();\n  }.property(),\n  \n  popup: nearestChild('popup-menu'),\n  \n  attachTargets: function () {\n    var popup = get(this, 'popup');\n    var icon = get(this, 'icon');\n\n    popup.addTarget(icon, {\n      on: \"click\"\n    });\n  }.on('didInsertElement'),\n  \n  actions: {\n    previousMonth: function () {\n      var previousMonth = get(this, 'firstOfMonth').clone().subtract(1, 'month');\n      set(this, 'month', previousMonth.month());\n      set(this, 'year', previousMonth.year());\n    },\n    \n    nextMonth: function () {\n      var nextMonth = get(this, 'firstOfMonth').clone().add(1, 'month');\n      set(this, 'month', nextMonth.month());\n      set(this, 'year', nextMonth.year());\n    },\n    \n    selectDate: function (date) {\n      set(this, 'value', date);\n      get(this, 'popup').deactivate();\n    }\n  },\n  \n  month: reads('currentMonth'),\n  year: reads('currentYear'),\n  \n  firstOfMonth: function () {\n    return moment({ year: get(this, 'year'), month: get(this, 'month') });\n  }.property('year', 'month'),\n\n  currentMonth: function () {\n    return get(this, 'value') ?\n           get(this, 'value').getMonth() :\n           new Date().getMonth();\n  }.property(),\n  \n  currentYear: function () {\n    return get(this, 'value') ?\n           get(this, 'value').getFullYear() :\n           new Date().getFullYear();\n  }.property(),\n  \n  displayValue: function () {\n    var value = get(this, 'value');\n    return value ? moment(value).format(\"MM/DD/YYYY\") : null;\n  }.property('value')\n});\n\nexport default DatePicker;\n```\n\nWhen we deactivate the popup, we're telling it that all targets are not active anymore. That way, the popup hides.\n\nTo polish it off, let's add styling. Create a file in addons called `styles/my-date-picker.css` and add the following CSS:\n\n```css\n.date-picker .popup-menu {\n  padding: 20px;\n}\n\n.date-picker header {\n  height: 25px;\n  position: relative;\n}\n.date-picker .next-month,\n.date-picker .previous-month {\n  cursor: pointer;\n  position: absolute;\n  top: 0;\n}\n\n.date-picker .next-month {\n  right: 0;\n}\n\n.date-picker .previous-month {\n  left: 0;\n}\n\n.date-picker .month {\n  text-align: center;\n}\n\n.calendar-month {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n\n.calendar-month th {\n  font-family: sans-serif;\n  text-transform: uppercase;\n  font-size: 12px;\n  height: 30px;\n  border-bottom: 1px solid #999;\n  border-top: 3px solid #FFF;\n  margin-bottom: 5px;\n}\n\n.calendar-day {\n  cursor: pointer;\n  text-align: center;\n  width: 20px;\n  height: 20px;\n  padding: 1px;\n}\n\n.calendar-day span {\n  display: block;\n  padding: 5px;\n}\n\n.calendar-day.disabled {\n  color: #999;\n  cursor: default;\n}\n\n.calendar-day.is-today span {\n  border: 1px solid #666;\n}\n\n.calendar-day.selected span {\n  border: 1px solid #FFF;\n}\n```\n\nIf everything went well, you should have a date-picker that behaves like the one here: http://paddle8.github.io/ember-popup-menu/\n\n\n## Running\n\n* `ember server`\n* Visit your app at http://localhost:4200.\n\n## Running Tests\n\n* `ember test`\n* `ember test --server`\n\n## Building\n\n* `ember build`\n\nFor more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronrenner%2Fember-popup-menu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faaronrenner%2Fember-popup-menu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronrenner%2Fember-popup-menu/lists"}