diff --git a/addon/components/_has-dom.ts b/addon/components/_has-dom.ts new file mode 100644 index 0000000..7928623 --- /dev/null +++ b/addon/components/_has-dom.ts @@ -0,0 +1,19 @@ +// check if window exists and actually is the global +export default typeof self === 'object' && + self !== null && + (self as Window['self']).Object === Object && + typeof Window !== 'undefined' && + self.constructor === Window && + typeof document === 'object' && + document !== null && + self.document === document && + typeof location === 'object' && + location !== null && + self.location === location && + typeof history === 'object' && + history !== null && + self.history === history && + typeof navigator === 'object' && + navigator !== null && + self.navigator === navigator && + typeof navigator.userAgent === 'string'; diff --git a/addon/components/_internals.ts b/addon/components/_internals.ts new file mode 100644 index 0000000..e600f9e --- /dev/null +++ b/addon/components/_internals.ts @@ -0,0 +1,78 @@ +function intern(str: string): string { + let obj = {}; + //@ts-ignore + obj[str] = 1; + for (let key in obj) { + if (key === str) { + return key; + } + } + return str; +} + +const GUID_KEY = intern(`__ember${Date.now()}`); + +// `enumerableSymbol` copied from https://github.com/emberjs/ember.js/blob/master/packages/@ember/-internals/utils/lib/symbol.ts +// for not exported code these legacy components are dependant on. + +// Some legacy symbols still need to be enumerable for a variety of reasons. +// This code exists for that, and as a fallback in IE11. In general, prefer +// `symbol` below when creating a new symbol. +function enumerableSymbol(debugName: string): string { + let id = GUID_KEY + Math.floor(Math.random() * Date.now()); + let symbol = intern(`__${debugName}${id}__`); + return symbol; +} + +export const HAS_BLOCK = enumerableSymbol('HAS_BLOCK'); + +export function isSimpleClick(event: MouseEvent): boolean { + let modifier = + event.shiftKey || event.metaKey || event.altKey || event.ctrlKey; + let secondaryClick = event.which > 1; // IE9 may return undefined + + return !modifier && !secondaryClick; +} + +// export interface GlobalContext { +// imports: object; +// exports: object; +// lookup: object; +// } + +// /* globals global, window, self */ +// declare const mainContext: object | undefined; + +// // from lodash to catch fake globals +// function checkGlobal(value: any | null | undefined): value is object { +// return value && value.Object === Object ? value : undefined; +// } + +// // element ids can ruin global miss checks +// function checkElementIdShadowing(value: any | null | undefined) { +// return value && value.nodeType === undefined ? value : undefined; +// } + +// // export real global +// export default checkGlobal(checkElementIdShadowing(typeof global === 'object' && global)) || +// checkGlobal(typeof self === 'object' && self) || +// checkGlobal(typeof window === 'object' && window) || +// (typeof mainContext !== 'undefined' && mainContext) || // set before strict mode in Ember loader/wrapper +// new Function('return this')(); // eval outside of strict mode + +// // legacy imports/exports/lookup stuff (should we keep this??) +// export const context = (function ( +// global: object, +// Ember: Partial | undefined +// ): GlobalContext { +// return Ember === undefined +// ? { imports: global, exports: global, lookup: global } +// : { +// // import jQuery +// imports: Ember.imports || global, +// // export Ember +// exports: Ember.exports || global, +// // search for Namespaces +// lookup: Ember.lookup || global, +// }; +// })(global, global.Ember); diff --git a/addon/components/checkbox.hbs b/addon/components/checkbox.hbs new file mode 100644 index 0000000..fb5c4b1 --- /dev/null +++ b/addon/components/checkbox.hbs @@ -0,0 +1 @@ +{{yield}} \ No newline at end of file diff --git a/addon/components/checkbox.js b/addon/components/checkbox.js new file mode 100644 index 0000000..8baae06 --- /dev/null +++ b/addon/components/checkbox.js @@ -0,0 +1,162 @@ +//@ts-check +/* eslint-disable ember/no-component-lifecycle-hooks */ +/* eslint-disable ember/require-tagless-components */ +/* eslint-disable ember/no-classic-classes */ +/* eslint-disable ember/no-classic-components */ +import { set } from '@ember/object'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import EmberComponent from '@ember/component'; + +/** +@module @ember/component +*/ + +/** + The internal class used to create text inputs when the `{{input}}` + helper is used with `type` of `checkbox`. + + See [Ember.Templates.helpers.input](/ember/release/classes/Ember.Templates.helpers/methods/input?anchor=input) for usage details. + + ## Direct manipulation of `checked` + + The `checked` attribute of an `Checkbox` object should always be set + through the Ember object or by interacting with its rendered element + representation via the mouse, keyboard, or touch. Updating the value of the + checkbox via jQuery will result in the checked value of the object and its + element losing synchronization. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. + + @class Checkbox + @public +*/ +const Checkbox = EmberComponent.extend({ + /** + By default, this component will add the `ember-checkbox` class to the component's element. + + @property classNames + @type Array | String + @default ['ember-checkbox'] + @public + */ + classNames: ['ember-checkbox'], + + tagName: 'input', + + /** + By default this component will forward a number of arguments to attributes on the the + component's element: + + * indeterminate + * disabled + * tabindex + * name + * autofocus + * required + * form + + When invoked with curly braces, this is the exhaustive list of HTML attributes you can + customize (i.e. `{{input type="checkbox" disabled=true}}`). + + When invoked with angle bracket invocation, this list is irrelevant, because you can use HTML + attribute syntax to customize the element (i.e. + ``). However, `@type` and + `@checked` must be passed as named arguments, not attributes. + + @property attributeBindings + @type Array | String + @default ['type', 'checked', 'indeterminate', 'disabled', 'tabindex', 'name', 'autofocus', 'required', 'form'] + @public + */ + attributeBindings: [ + 'type', + 'checked', + 'indeterminate', + 'disabled', + 'tabindex', + 'name', + 'autofocus', + 'required', + 'form', + ], + + /** + Sets the `type` attribute of the `Checkbox`'s element + + @property disabled + @default false + @private + */ + type: 'checkbox', + + /** + Sets the `disabled` attribute of the `Checkbox`'s element + + @property disabled + @default false + @public + */ + disabled: false, + + /** + Corresponds to the `indeterminate` property of the `Checkbox`'s element + + @property disabled + @default false + @public + */ + indeterminate: false, + + /** + Whenever the checkbox is inserted into the DOM, perform initialization steps, which include + setting the indeterminate property if needed. + + If this method is overridden, `super` must be called. + + @method + @public + */ + didInsertElement() { + this._super(...arguments); + this.element.indeterminate = Boolean(this.indeterminate); + }, + + /** + Whenever the `change` event is fired on the checkbox, update its `checked` property to reflect + whether the checkbox is checked. + + If this method is overridden, `super` must be called. + + @method + @public + */ + change() { + set(this, 'checked', this.element.checked); + }, +}); + +if (DEBUG) { + const UNSET = {}; + + Checkbox.reopen({ + value: UNSET, + + didReceiveAttrs() { + this._super(); + + assert( + "`` is not supported; " + + "please use `` instead.", + !(this.type === 'checkbox' && this.value !== UNSET) + ); + }, + }); +} + +Checkbox.toString = () => '@ember/component/checkbox'; + +export default Checkbox; diff --git a/addon/components/link-to.hbs b/addon/components/link-to.hbs new file mode 100644 index 0000000..e67d811 --- /dev/null +++ b/addon/components/link-to.hbs @@ -0,0 +1,5 @@ +{{~#if (has-block)~}} + {{yield}} +{{~else~}} + {{this.linkTitle}} +{{~/if~}} diff --git a/addon/components/link-to.ts b/addon/components/link-to.ts new file mode 100644 index 0000000..5b759b0 --- /dev/null +++ b/addon/components/link-to.ts @@ -0,0 +1,1063 @@ +import { alias } from '@ember/object/computed'; +import { computed } from '@ember/object'; +import { getOwner } from '@ember/application'; +import EmberComponent from '@ember/component'; +import { assert, deprecate, runInDebug, warn } from '@ember/debug'; +import { getEngineParent } from '@ember/engine'; +import EngineInstance from '@ember/engine/instance'; +import { inject as injectService } from '@ember/service'; +import { DEBUG } from '@glimmer/env'; +import { isSimpleClick } from './_internals'; +import { HAS_BLOCK } from './_internals'; + +/** + The `LinkTo` component renders a link to the supplied `routeName` passing an optionally + supplied model to the route as its `model` context of the route. The block for `LinkTo` + becomes the contents of the rendered element: + + ```handlebars + + Great Hamster Photos + + ``` + + This will result in: + + ```html + + Great Hamster Photos + + ``` + + ### Disabling the `LinkTo` component + + The `LinkTo` component can be disabled by using the `disabled` argument. A disabled link + doesn't result in a transition when activated, and adds the `disabled` class to the `` + element. + + (The class name to apply to the element can be overridden by using the `disabledClass` + argument) + + ```handlebars + + Great Hamster Photos + + ``` + + ### Handling `href` + + `` will use your application's Router to fill the element's `href` property with a URL + that matches the path to the supplied `routeName`. + + ### Handling current route + + The `LinkTo` component will apply a CSS class name of 'active' when the application's current + route matches the supplied routeName. For example, if the application's current route is + 'photoGallery.recent', then the following invocation of `LinkTo`: + + ```handlebars + + Great Hamster Photos + + ``` + + will result in + + ```html + + Great Hamster Photos + + ``` + + The CSS class used for active classes can be customized by passing an `activeClass` argument: + + ```handlebars + + Great Hamster Photos + + ``` + + ```html + + Great Hamster Photos + + ``` + + ### Keeping a link active for other routes + + If you need a link to be 'active' even when it doesn't match the current route, you can use the + `current-when` argument. + + ```handlebars + + Photo Gallery + + ``` + + This may be helpful for keeping links active for: + + * non-nested routes that are logically related + * some secondary menu approaches + * 'top navigation' with 'sub navigation' scenarios + + A link will be active if `current-when` is `true` or the current + route is the route this link would transition to. + + To match multiple routes 'space-separate' the routes: + + ```handlebars + + Art Gallery + + ``` + + ### Supplying a model + + An optional `model` argument can be used for routes whose + paths contain dynamic segments. This argument will become + the model context of the linked route: + + ```javascript + Router.map(function() { + this.route("photoGallery", {path: "hamster-photos/:photo_id"}); + }); + ``` + + ```handlebars + + {{aPhoto.title}} + + ``` + + ```html + + Tomster + + ``` + + ### Supplying multiple models + + For deep-linking to route paths that contain multiple + dynamic segments, the `models` argument can be used. + + As the router transitions through the route path, each + supplied model argument will become the context for the + route with the dynamic segments: + + ```javascript + Router.map(function() { + this.route("photoGallery", { path: "hamster-photos/:photo_id" }, function() { + this.route("comment", {path: "comments/:comment_id"}); + }); + }); + ``` + + This argument will become the model context of the linked route: + + ```handlebars + + {{comment.body}} + + ``` + + ```html + + A+++ would snuggle again. + + ``` + + ### Supplying an explicit dynamic segment value + + If you don't have a model object available to pass to `LinkTo`, + an optional string or integer argument can be passed for routes whose + paths contain dynamic segments. This argument will become the value + of the dynamic segment: + + ```javascript + Router.map(function() { + this.route("photoGallery", { path: "hamster-photos/:photo_id" }); + }); + ``` + + ```handlebars + + {{this.aPhoto.title}} + + ``` + + ```html + + Tomster + + ``` + + When transitioning into the linked route, the `model` hook will + be triggered with parameters including this passed identifier. + + ### Supplying a `tagName` + + By default `` renders an `` element. This can be overridden for a single use of + `` by supplying a `tagName` argument: + + ```handlebars + + Great Hamster Photos + + ``` + + This produces: + + ```html +
  • + Great Hamster Photos +
  • + ``` + + In general, this is not recommended. + + ### Supplying query parameters + + If you need to add optional key-value pairs that appear to the right of the ? in a URL, + you can use the `query` argument. + + ```handlebars + + Great Hamster Photos + + ``` + + This will result in: + + ```html +
    + Great Hamster Photos + + ``` + + @for Ember.Templates.components + @method LinkTo + @see {LinkComponent} + @public +*/ + +/** + @module @ember/routing +*/ + +/** + See [Ember.Templates.components.LinkTo](/ember/release/classes/Ember.Templates.components/methods/input?anchor=LinkTo). + + @for Ember.Templates.helpers + @method link-to + @see {Ember.Templates.components.LinkTo} + @public +**/ + +/** + `LinkComponent` is the internal component invoked with `` or `{{link-to}}`. + + @class LinkComponent + @extends Component + @see {Ember.Templates.components.LinkTo} + @public +**/ + +const UNDEFINED = Object.freeze({ + toString() { + return 'UNDEFINED'; + }, +}); + +const EMPTY_QUERY_PARAMS = Object.freeze({}); + +const LinkComponent = EmberComponent.extend({ + tagName: 'a', + + /** + @property route + @public + */ + route: UNDEFINED, + + /** + @property model + @public + */ + model: UNDEFINED, + + /** + @property models + @public + */ + models: UNDEFINED, + + /** + @property query + @public + */ + query: UNDEFINED, + + /** + Used to determine when this `LinkComponent` is active. + + @property current-when + @public + */ + 'current-when': null, + + /** + Sets the `title` attribute of the `LinkComponent`'s HTML element. + + @property title + @default null + @public + **/ + title: null, + + /** + Sets the `rel` attribute of the `LinkComponent`'s HTML element. + + @property rel + @default null + @public + **/ + rel: null, + + /** + Sets the `tabindex` attribute of the `LinkComponent`'s HTML element. + + @property tabindex + @default null + @public + **/ + tabindex: null, + + /** + Sets the `target` attribute of the `LinkComponent`'s HTML element. + + @since 1.8.0 + @property target + @default null + @public + **/ + target: null, + + /** + The CSS class to apply to `LinkComponent`'s element when its `active` + property is `true`. + + @property activeClass + @type String + @default active + @public + **/ + activeClass: 'active', + + /** + The CSS class to apply to `LinkComponent`'s element when its `loading` + property is `true`. + + @property loadingClass + @type String + @default loading + @public + **/ + loadingClass: 'loading', + + /** + The CSS class to apply to a `LinkComponent`'s element when its `disabled` + property is `true`. + + @property disabledClass + @type String + @default disabled + @public + **/ + disabledClass: 'disabled', + + /** + Determines whether the `LinkComponent` will trigger routing via + the `replaceWith` routing strategy. + + @property replace + @type Boolean + @default false + @public + **/ + replace: false, + + /** + Determines whether the `LinkComponent` will prevent the default + browser action by calling preventDefault() to avoid reloading + the browser page. + + If you need to trigger a full browser reload pass `@preventDefault={{false}}`: + + ```handlebars + + {{this.aPhotoId.title}} + + ``` + + @property preventDefault + @type Boolean + @default true + @private + **/ + preventDefault: true, + + /** + By default this component will forward `href`, `title`, `rel`, `tabindex`, and `target` + arguments to attributes on the component's element. When invoked with `{{link-to}}`, you can + only customize these attributes. When invoked with ``, you can just use HTML + attributes directly. + + @property attributeBindings + @type Array | String + @default ['title', 'rel', 'tabindex', 'target'] + @public + */ + attributeBindings: ['href', 'title', 'rel', 'tabindex', 'target'], + + /** + By default this component will set classes on its element when any of the following arguments + are truthy: + + * active + * loading + * disabled + + When these arguments are truthy, a class with the same name will be set on the element. When + falsy, the associated class will not be on the element. + + @property classNameBindings + @type Array + @default ['active', 'loading', 'disabled', 'ember-transitioning-in', 'ember-transitioning-out'] + @public + */ + classNameBindings: [ + 'active', + 'loading', + 'disabled', + 'transitioningIn', + 'transitioningOut', + ], + + /** + By default this component responds to the `click` event. When the component element is an + `` element, activating the link in another way, such as using the keyboard, triggers the + click event. + + @property eventName + @type String + @default click + @private + */ + eventName: 'click', + + // this is doc'ed here so it shows up in the events + // section of the API documentation, which is where + // people will likely go looking for it. + /** + Triggers the `LinkComponent`'s routing behavior. If + `eventName` is changed to a value other than `click` + the routing behavior will trigger on that custom event + instead. + + @event click + @private + */ + + /** + An overridable method called when `LinkComponent` objects are instantiated. + + Example: + + ```app/components/my-link.js + import LinkComponent from '@ember/routing/link-component'; + + export default LinkComponent.extend({ + init() { + this._super(...arguments); + console.log('Event is ' + this.get('eventName')); + } + }); + ``` + + NOTE: If you do override `init` for a framework class like `Component`, + be sure to call `this._super(...arguments)` in your + `init` declaration! If you don't, Ember may not have an opportunity to + do important setup work, and you'll see strange behavior in your + application. + + @method init + @private + */ + init() { + this._super(...arguments); + + assert( + 'You attempted to use the component within a routeless engine, this is not supported. ' + + 'If you are using the ember-engines addon, use the component instead. ' + + 'See https://ember-engines.com/docs/links for more info.', + !this._isEngine || this._engineMountPoint !== undefined + ); + + // Map desired event name to invoke function + let { eventName } = this; + this.on(eventName, this, this._invoke); + }, + //@ts-ignore + _routing: injectService('-routing'), + _currentRoute: alias('_routing.currentRouteName'), + _currentRouterState: alias('_routing.currentState'), + _targetRouterState: alias('_routing.targetState'), + + _isEngine: computed(function (this: any) { + return getEngineParent(getOwner(this) as EngineInstance) !== undefined; + }), + + _engineMountPoint: computed(function (this: any) { + //@ts-ignore + return (getOwner(this) as EngineInstance).mountPoint; + }), + + _route: computed( + 'route', + '_currentRouterState', + function computeLinkToComponentRoute(this: any) { + let { route } = this; + + return route === UNDEFINED + ? this._currentRoute + : this._namespaceRoute(route); + } + ), + + _models: computed( + 'model', + 'models', + function computeLinkToComponentModels(this: any) { + let { model, models } = this; + + assert( + 'You cannot provide both the `@model` and `@models` arguments to the component.', + model === UNDEFINED || models === UNDEFINED + ); + + if (model !== UNDEFINED) { + return [model]; + } else if (models !== UNDEFINED) { + assert( + 'The `@models` argument must be an array.', + Array.isArray(models) + ); + return models; + } else { + return []; + } + } + ), + + _query: computed('query', function computeLinkToComponentQuery(this: any) { + let { query } = this; + + if (query === UNDEFINED) { + return EMPTY_QUERY_PARAMS; + } else { + return Object.assign({}, query); + } + }), + + /** + Accessed as a classname binding to apply the component's `disabledClass` + CSS `class` to the element when the link is disabled. + + When `true`, interactions with the element will not trigger route changes. + @property disabled + @public + */ + disabled: computed({ + get(_key: string): boolean { + // always returns false for `get` because (due to the `set` just below) + // the cached return value from the set will prevent this getter from _ever_ + // being called after a set has occurred + return false; + }, + + set(this: any, _key: string, value: any): boolean { + this._isDisabled = value; + + return value ? this.disabledClass : false; + }, + }), + + /** + Accessed as a classname binding to apply the component's `activeClass` + CSS `class` to the element when the link is active. + + This component is considered active when its `currentWhen` property is `true` + or the application's current route is the route this component would trigger + transitions into. + + The `currentWhen` property can match against multiple routes by separating + route names using the ` ` (space) character. + + @property active + @private + */ + active: computed( + 'activeClass', + '_active', + function computeLinkToComponentActiveClass(this: any) { + return this._active ? this.activeClass : false; + } + ), + + _active: computed( + '_currentRouterState', + '_route', + '_models', + '_query', + 'loading', + 'current-when', + function computeLinkToComponentActive(this: any) { + let { _currentRouterState: state } = this; + + if (state) { + return this._isActive(state); + } else { + return false; + } + } + ), + + willBeActive: computed( + '_currentRouterState', + '_targetRouterState', + '_route', + '_models', + '_query', + 'loading', + 'current-when', + function computeLinkToComponentWillBeActive(this: any) { + let { _currentRouterState: current, _targetRouterState: target } = this; + + if (current === target) { + return; + } + + return this._isActive(target); + } + ), + + _isActive(routerState: any /* RouterState */): boolean { + if (this.loading) { + return false; + } + + let currentWhen = this['current-when']; + + if (typeof currentWhen === 'boolean') { + return currentWhen; + } + + let { _models: models, _routing: routing } = this; + + if (typeof currentWhen === 'string') { + return currentWhen + .split(' ') + .some((route) => + routing.isActiveForRoute( + models, + undefined, + this._namespaceRoute(route), + routerState + ) + ); + } else { + return routing.isActiveForRoute( + models, + this._query, + this._route, + routerState + ); + } + }, + + transitioningIn: computed( + '_active', + 'willBeActive', + function computeLinkToComponentTransitioningIn(this: any) { + if (this.willBeActive === true && !this._active) { + return 'ember-transitioning-in'; + } else { + return false; + } + } + ), + + transitioningOut: computed( + '_active', + 'willBeActive', + function computeLinkToComponentTransitioningOut(this: any) { + if (this.willBeActive === false && this._active) { + return 'ember-transitioning-out'; + } else { + return false; + } + } + ), + + _namespaceRoute(route: string): string { + let { _engineMountPoint: mountPoint } = this; + + if (mountPoint === undefined) { + return route; + } else if (route === 'application') { + return mountPoint; + } else { + return `${mountPoint}.${route}`; + } + }, + + /** + Event handler that invokes the link, activating the associated route. + + @method _invoke + @param {Event} event + @private + */ + _invoke(this: any, event: Event): boolean { + if (!isSimpleClick(event)) { + return true; + } + + let { bubbles, preventDefault } = this; + let target = this.element.target; + let isSelf = !target || target === '_self'; + + if (preventDefault !== false && isSelf) { + event.preventDefault(); + } + + if (bubbles === false) { + event.stopPropagation(); + } + + if (this._isDisabled) { + return false; + } + + if (this.loading) { + // tslint:disable-next-line:max-line-length + warn( + 'This link is in an inactive loading state because at least one of its models ' + + 'currently has a null/undefined value, or the provided route name is invalid.', + false, + { + id: 'ember-glimmer.link-to.inactive-loading-state', + } + ); + return false; + } + + if (!isSelf) { + return false; + } + + let { + _route: routeName, + _models: models, + _query: queryParams, + replace: shouldReplace, + } = this; + + let payload = { + queryParams, + routeName, + }; + + // flaggedInstrument( + // 'interaction.link-to', + // payload, + // this._generateTransition( + // payload, + // routeName, + // models, + // queryParams, + // shouldReplace + // ) + // ); + this._generateTransition( + payload, + routeName, + models, + queryParams, + shouldReplace + ); + return false; + }, + + _generateTransition( + payload: any, + qualifiedRouteName: string, + models: any[], + queryParams: any[], + shouldReplace: boolean + ) { + let { _routing: routing } = this; + + return () => { + payload.transition = routing.transitionTo( + qualifiedRouteName, + models, + queryParams, + shouldReplace + ); + }; + }, + + /** + Sets the element's `href` attribute to the url for + the `LinkComponent`'s targeted route. + + If the `LinkComponent`'s `tagName` is changed to a value other + than `a`, this property will be ignored. + + @property href + @private + */ + href: computed( + '_currentRouterState', + '_route', + '_models', + '_query', + 'tagName', + 'loading', + 'loadingHref', + function computeLinkToComponentHref(this: any) { + if (this.tagName !== 'a') { + return; + } + + if (this.loading) { + return this.loadingHref; + } + + let { + _route: route, + _models: models, + _query: query, + _routing: routing, + } = this; + + if (DEBUG) { + /* + * Unfortunately, to get decent error messages, we need to do this. + * In some future state we should be able to use a "feature flag" + * which allows us to strip this without needing to call it twice. + * + * if (isDebugBuild()) { + * // Do the useful debug thing, probably including try/catch. + * } else { + * // Do the performant thing. + * } + */ + try { + return routing.generateURL(route, models, query); + } catch (e) { + // tslint:disable-next-line:max-line-length + e.message = `While generating link to route "${this.route}": ${e.message}`; + throw e; + } + } else { + return routing.generateURL(route, models, query); + } + } + ), + + loading: computed( + '_route', + '_modelsAreLoaded', + 'loadingClass', + function computeLinkToComponentLoading(this: any) { + let { _route: route, _modelsAreLoaded: loaded } = this; + + if (!loaded || route === null || route === undefined) { + return this.loadingClass; + } + } + ), + + _modelsAreLoaded: computed( + '_models', + function computeLinkToComponentModelsAreLoaded(this: any) { + let { _models: models } = this; + + for (let i = 0; i < models.length; i++) { + let model = models[i]; + if (model === null || model === undefined) { + return false; + } + } + + return true; + } + ), + + /** + The default href value to use while a link-to is loading. + Only applies when tagName is 'a' + + @property loadingHref + @type String + @default # + @private + */ + loadingHref: '#', + + didReceiveAttrs() { + let { disabledWhen } = this; + + if (disabledWhen !== undefined) { + this.set('disabled', disabledWhen); + } + + let { params } = this; + + if (!params || params.length === 0) { + assert( + 'You must provide at least one of the `@route`, `@model`, `@models` or `@query` argument to ``.', + !( + this.route === UNDEFINED && + this.model === UNDEFINED && + this.models === UNDEFINED && + this.query === UNDEFINED + ) + ); + + let { _models: models } = this; + if (models.length > 0) { + let lastModel = models[models.length - 1]; + + if ( + typeof lastModel === 'object' && + lastModel !== null && + lastModel.isQueryParams + ) { + this.query = lastModel.values; + models.pop(); + } + } + + return; + } + + let hasBlock = this[HAS_BLOCK]; + + params = params.slice(); + + // Process the positional arguments, in order. + // 1. Inline link title comes first, if present. + if (!hasBlock) { + this.set('linkTitle', params.shift()); + } + + // 2. The last argument is possibly the `query` object. + let queryParams = params[params.length - 1]; + + if (queryParams && queryParams.isQueryParams) { + this.set('query', params.pop().values); + } else { + this.set('query', UNDEFINED); + } + + // 3. If there is a `route`, it is now at index 0. + if (params.length === 0) { + this.set('route', UNDEFINED); + } else { + this.set('route', params.shift()); + } + + // 4. Any remaining indices (if any) are `models`. + this.set('model', UNDEFINED); + this.set('models', params); + + runInDebug(() => { + params = this.params.slice(); + + let equivalentNamedArgs = []; + let hasQueryParams = false; + + // Process the positional arguments, in order. + // 1. Inline link title comes first, if present. + if (!hasBlock) { + params.shift(); + } + + // 2. The last argument is possibly the `query` object. + let query = params[params.length - 1]; + + if (query && query.isQueryParams) { + params.pop(); + hasQueryParams = true; + } + + // 3. If there is a `route`, it is now at index 0. + if (params.length > 0) { + params.shift(); + equivalentNamedArgs.push('`@route`'); + } + + // 4. Any remaining params (if any) are `models`. + if (params.length === 1) { + equivalentNamedArgs.push('`@model`'); + } else if (params.length > 1) { + equivalentNamedArgs.push('`@models`'); + } + + if (hasQueryParams) { + equivalentNamedArgs.push('`@query`'); + } + + if (equivalentNamedArgs.length > 0) { + let message = + 'Invoking the `` component with positional arguments is deprecated.'; + + message += `Please use the equivalent named arguments (${equivalentNamedArgs.join( + ', ' + )})`; + + if (hasQueryParams) { + message += ' along with the `hash` helper'; + } + + if (!hasBlock) { + message += " and pass a block for the link's content."; + } + + message += '.'; + + deprecate(message, false, { + id: 'ember-glimmer.link-to.positional-arguments', + until: '4.0.0', + for: 'ember-source', + url: + 'https://deprecations.emberjs.com/v3.x#toc_ember-glimmer-link-to-positional-arguments', + since: { + enabled: '3.26.0-beta.1', + }, + }); + } + }); + }, +}); + +LinkComponent.toString = () => '@ember/routing/link-component'; + +LinkComponent.reopenClass({ + positionalParams: 'params', +}); + +export default LinkComponent; diff --git a/addon/components/text-field.ts b/addon/components/text-field.ts new file mode 100644 index 0000000..143f9f3 --- /dev/null +++ b/addon/components/text-field.ts @@ -0,0 +1,204 @@ +/* eslint-disable ember/no-mixins */ +/* eslint-disable ember/no-classic-classes */ +/** +@module @ember/component +*/ + +import hasDOM from './_has-dom'; +import { computed } from '@ember/object'; +import Component from '@ember/component'; +import TextSupport from '../mixins/text-support'; + +const inputTypes = hasDOM ? Object.create(null) : null; +function canSetTypeOfInput(type: string): boolean { + // if running in outside of a browser always return + // the original type + if (!hasDOM) { + return Boolean(type); + } + + if (type in inputTypes) { + return inputTypes[type]; + } + + let inputTypeTestElement = document.createElement('input'); + + try { + inputTypeTestElement.type = type; + } catch (e) { + // ignored + } + + return (inputTypes[type] = inputTypeTestElement.type === type); +} + +/** + The internal class used to create text inputs when the `Input` component is used with `type` of `text`. + + See [Ember.Templates.components.Input](/ember/release/classes/Ember.Templates.components/methods/Input?anchor=Input) for usage details. + + ## Layout and LayoutName properties + + Because HTML `input` elements are self closing `layout` and `layoutName` + properties will not be applied. + + @class TextField + @extends Component + @uses Ember.TextSupport + @public +*/ +const TextField = Component.extend(TextSupport, { + /** + By default, this component will add the `ember-text-field` class to the component's element. + + @property classNames + @type Array | String + @default ['ember-text-field'] + @public + */ + classNames: ['ember-text-field'], + tagName: 'input', + + /** + By default this component will forward a number of arguments to attributes on the the + component's element: + + * accept + * autocomplete + * autosave + * dir + * formaction + * formenctype + * formmethod + * formnovalidate + * formtarget + * height + * inputmode + * lang + * list + * type + * max + * min + * multiple + * name + * pattern + * size + * step + * value + * width + + When invoked with `{{input type="text"}}`, you can only customize these attributes. When invoked + with ``, you can just use HTML attributes directly. + + @property attributeBindings + @type Array | String + @default ['accept', 'autocomplete', 'autosave', 'dir', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'height', 'inputmode', 'lang', 'list', 'type', 'max', 'min', 'multiple', 'name', 'pattern', 'size', 'step', 'value', 'width'] + @public + */ + attributeBindings: [ + 'accept', + 'autocomplete', + 'autosave', + 'dir', + 'formaction', + 'formenctype', + 'formmethod', + 'formnovalidate', + 'formtarget', + 'height', + 'inputmode', + 'lang', + 'list', + 'type', // needs to be before min and max. See #15675 + 'max', + 'min', + 'multiple', + 'name', + 'pattern', + 'size', + 'step', + 'value', + 'width', + ], + + /** + As the user inputs text, this property is updated to reflect the `value` property of the HTML + element. + + @property value + @type String + @default "" + @public + */ + value: '', + + /** + The `type` attribute of the input element. + + @property type + @type String + @default "text" + @public + */ + type: computed({ + get(): string { + return 'text'; + }, + + set(_key: string, value: string) { + let type = 'text'; + + if (canSetTypeOfInput(value)) { + type = value; + } + + return type; + }, + }), + + /** + The `size` of the text field in characters. + + @property size + @type String + @default null + @public + */ + size: null, + + /** + The `pattern` attribute of input element. + + @property pattern + @type String + @default null + @public + */ + pattern: null, + + /** + The `min` attribute of input element used with `type="number"` or `type="range"`. + + @property min + @type String + @default null + @since 1.4.0 + @public + */ + min: null, + + /** + The `max` attribute of input element used with `type="number"` or `type="range"`. + + @property max + @type String + @default null + @since 1.4.0 + @public + */ + max: null, +}); + +TextField.toString = () => '@ember/component/text-field'; + +export default TextField; diff --git a/addon/components/textarea.js b/addon/components/textarea.js new file mode 100644 index 0000000..b5a94f4 --- /dev/null +++ b/addon/components/textarea.js @@ -0,0 +1,166 @@ +/* eslint-disable ember/no-mixins */ +/* eslint-disable ember/require-tagless-components */ +/** +@module @ember/component +*/ +import TextSupport from '../mixins/text-support'; +import Component from '@ember/component'; +import layout from '../templates/empty'; + +/** + The `Textarea` component inserts a new instance of ` + ``` + + The `@value` argument is two-way bound. If the user types text into the textarea, the `@value` + argument is updated. If the `@value` argument is updated, the text in the textarea is updated. + + In the following example, the `writtenWords` property on the component will be updated as the user + types 'Lots of text' into the text area of their browser's window. + + ```app/components/word-editor.js + import Component from '@glimmer/component'; + import { tracked } from '@glimmer/tracking'; + + export default class WordEditorComponent extends Component { + @tracked writtenWords = "Lots of text that IS bound"; + } + ``` + + ```handlebars + + ``` + + If you wanted a one way binding, you could use the `