Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,047 changes: 0 additions & 1,047 deletions packages/interactivity/src/directives.tsx

This file was deleted.

95 changes: 95 additions & 0 deletions packages/interactivity/src/directives/bind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { type RefObject } from 'preact';
import { directive, isNonDefaultDirectiveSuffix } from '../hooks';
import { useInit } from '../utils';
import { PENDING_GETTER } from '../proxies/state';
import { warnUniqueIdNotSupported } from './utils/warnings';

// data-wp-bind--[attribute] binding.
directive( 'bind', ( { directives: { bind }, element, evaluate } ) => {
bind.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => {
if ( entry.uniqueId ) {
if ( globalThis.SCRIPT_DEBUG ) {
warnUniqueIdNotSupported( 'bind', entry.uniqueId );
}
return;
}
const attribute = entry.suffix;
let result = evaluate( entry );
if ( result === PENDING_GETTER ) {
return;
}
if ( typeof result === 'function' ) {
result = result();
}
element.props[ attribute ] = result;

/*
* This is necessary because Preact doesn't change the attributes on the
* hydration, so we have to do it manually. It only needs to do it the
* first time. After that, Preact will handle the changes.
*/
useInit( () => {
const el = ( element.ref as RefObject< HTMLElement > ).current!;

/*
* We set the value directly to the corresponding HTMLElement instance
* property excluding the following special cases. We follow Preact's
* logic: https://github.com/preactjs/preact/blob/10.29.1/src/diff/props.js#L115-L129
*/
if ( attribute === 'style' ) {
if ( typeof result === 'string' ) {
el.style.cssText = result;
}
return;
} else if (
attribute !== 'width' &&
attribute !== 'height' &&
attribute !== 'href' &&
attribute !== 'list' &&
attribute !== 'form' &&
/*
* The value for `tabindex` follows the parsing rules for an
* integer. If that fails, or if the attribute isn't present, then
* the browsers should "follow platform conventions to determine if
* the element should be considered as a focusable area",
* practically meaning that most elements get a default of `-1` (not
* focusable), but several also get a default of `0` (focusable in
* order after all elements with a positive `tabindex` value).
*
* @see https://html.spec.whatwg.org/#tabindex-value
*/
attribute !== 'tabIndex' &&
attribute !== 'download' &&
attribute !== 'rowSpan' &&
attribute !== 'colSpan' &&
attribute !== 'role' &&
attribute !== 'popover' &&
attribute in el
) {
try {
el[ attribute ] =
result === null || result === undefined ? '' : result;
return;
} catch {}
}
/*
* aria- and data- attributes have no boolean representation.
* A `false` value is different from the attribute not being
* present, so we can't remove it.
* We follow Preact's logic: https://github.com/preactjs/preact/blob/10.29.1/src/diff/props.js#L138-L150
*/
if (
result !== null &&
result !== undefined &&
( result !== false || attribute[ 4 ] === '-' )
) {
el.setAttribute(
attribute,
attribute === 'popover' && result === true ? '' : result
);
} else {
el.removeAttribute( attribute );
}
} );
} );
} );
54 changes: 54 additions & 0 deletions packages/interactivity/src/directives/class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { type RefObject } from 'preact';
import { directive, isNonDefaultDirectiveSuffix } from '../hooks';
import { useInit } from '../utils';
import { PENDING_GETTER } from '../proxies/state';

// data-wp-class--[classname] Dynamic class binding.
directive(
'class',
( { directives: { class: classNames }, element, evaluate } ) => {
classNames.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => {
const className = entry.uniqueId
? `${ entry.suffix }---${ entry.uniqueId }`
: entry.suffix;
let result = evaluate( entry );
if ( result === PENDING_GETTER ) {
return;
}
if ( typeof result === 'function' ) {
result = result();
}
const currentClass = element.props.class || '';
const classFinder = new RegExp(
`(^|\\s)${ className }(\\s|$)`,
'g'
);
if ( ! result ) {
element.props.class = currentClass
.replace( classFinder, ' ' )
.trim();
} else if ( ! classFinder.test( currentClass ) ) {
element.props.class = currentClass
? `${ currentClass } ${ className }`
: className;
}

useInit( () => {
/*
* This seems necessary because Preact doesn't change the class
* names on the hydration, so we have to do it manually. It doesn't
* need deps because it only needs to do it the first time.
*/
if ( ! result ) {
(
element.ref as RefObject< HTMLElement >
).current!.classList.remove( className );
} else {
(
element.ref as RefObject< HTMLElement >
).current!.classList.add( className );
}
} );
} );
}
);
85 changes: 85 additions & 0 deletions packages/interactivity/src/directives/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { createElement } from 'preact';
import { useContext, useRef } from 'preact/hooks';
import { directive, isDefaultDirectiveSuffix } from '../hooks';
import { warn, isPlainObject, deepClone } from '../utils';
import { proxifyState, proxifyContext, deepMerge } from '../proxies';

// data-wp-context---[unique-id] — Local state scoping with DOM inheritance.
directive(
'context',
( {
directives: { context },
props: { children },
context: inheritedContext,
} ) => {
const entries = context
.filter( isDefaultDirectiveSuffix )
// Reverses entries to make the ones with unique IDs override the default one.
.reverse();

// Doesn't do anything if there are no default entries.
if ( ! entries.length ) {
if ( globalThis.SCRIPT_DEBUG ) {
warn(
'The usage of data-wp-context--unique-id (two hyphens) is not supported. To add a unique ID to the directive, please use data-wp-context---unique-id (three hyphens) instead.'
);
}
return;
}

const { Provider } = inheritedContext;
const { client: inheritedClient, server: inheritedServer } =
useContext( inheritedContext );
const client = useRef( {} );
const server = {};
const result = {
client: { ...inheritedClient },
server: { ...inheritedServer },
};
const namespaces = new Set< string >();

entries.forEach( ( { value, namespace, uniqueId } ) => {
// Checks that the value is a JSON object. Sends a console warning if not.
if ( ! isPlainObject( value ) ) {
if ( globalThis.SCRIPT_DEBUG ) {
warn(
`The value of data-wp-context${
uniqueId ? `---${ uniqueId }` : ''
} on the ${ namespace } namespace must be a valid stringified JSON object.`
);
}
return;
}

// If the namespace doesn't exist yet, initalizes an empty
// proxified state for that namespace's client context.
if ( ! client.current[ namespace ] ) {
client.current[ namespace ] = proxifyState( namespace, {} );
}

// Merges the new client value with whatever was there before.
deepMerge( client.current[ namespace ], deepClone( value ), false );

// Replaces the server context for that namespace with the
// current value.
server[ namespace ] = value;

// Registers the namespace.
namespaces.add( namespace );
} );

namespaces.forEach( ( namespace ) => {
result.client[ namespace ] = proxifyContext(
client.current[ namespace ],
inheritedClient[ namespace ]
);
result.server[ namespace ] = proxifyContext(
server[ namespace ],
inheritedServer[ namespace ]
);
} );

return createElement( Provider, { value: result }, children );
},
{ priority: 5 }
);
Loading
Loading